From fff8805a55fed09c1d33ea444ee545901681161f Mon Sep 17 00:00:00 2001 From: Jorge Gallegos Date: Thu, 4 Sep 2025 14:29:08 -0700 Subject: [PATCH] Set documentation the "right" way --- plugins/connection/iap.py | 588 ++++++++++++++++++++++++++++++++------ 1 file changed, 500 insertions(+), 88 deletions(-) diff --git a/plugins/connection/iap.py b/plugins/connection/iap.py index 8d66fdb1..37069867 100644 --- a/plugins/connection/iap.py +++ b/plugins/connection/iap.py @@ -1,7 +1,490 @@ # Copyright (c) 2025 Red Hat # GNU General Public License v3.0+ https://www.gnu.org/licenses/gpl-3.0.txt -from __future__ import absolute_import, annotations +# I had to duplicate (almost?) all of the documentation found in the +# ansible.plugins.connection.ssh plugin, due to how ansible-doc and ansible-test sanity +# work, they look at the lexical structure of the code. I had initially done: +# 1. load ssh.py DOCUMENTATION string into a yaml object +# 2. modify the yaml object to change defaults / add my options +# 3. set this plugin's DOCUMENTATION to the yaml.dump() of the modified object +# but that doesn't work with how the AST evaluation is done. +# here are the changes from upstream: +# 1. Changed default private_key_file default to ~/.ssh/google_compute_engine +# 2. Make host_key_checking default to False +# 3. Added known_hosts_file option pointing to ~/.ssh/google_compute_known_hosts +DOCUMENTATION = """ + name: gcloud-iap + short_description: connect via SSH through Google Cloud's Identity Aware Proxy (IAP) + description: + - This connection plugin behaves almost like the stock SSH plugin, but it creates + a new IAP process per host in the inventory so connections are tunneled through + it. + author: Jorge A Gallegos (jgallego@redhat.com) + notes: + - This plugin requires you to have configured gcloud authentication prior to using + it. You can change the active configuration used, but the plugin won't auth + for you. + - This plugin is mostly a wrapper to the ``ssh`` CLI utility and the exact behavior + of the options depends on this tool. This means that the documentation provided + here is subject to be overridden by the CLI tool itself. + - Many options default to V(None) here but that only means we do not override the + SSH tool's defaults and/or configuration. For example, if you specify the port + in this plugin it will override any C(Port) entry in your C(.ssh/config). + - The ssh CLI tool uses return code 255 as a 'connection error', this can conflict + with commands/tools that also return 255 as an error code and will look like an + 'unreachable' condition or 'connection error' to this plugin. + extends_documentation_fragment: + - connection_pipelining + options: + gcloud_executable: + description: + - Path to the gcloud executable, defaults to whatever is found in the PATH + environment variable. + type: string + vars: + - name: ansible_gcloud_executable + ini: + - section: gcloud + key: executable + gcloud_configuration: + description: + - If set, points to non-standard gcloud configuration. + type: string + vars: + - name: ansible_gcloud_configuration + ini: + - section: gcloud + key: configuration + env: + - name: CLOUDSDK_ACTIVE_CONFIG_NAME + gcloud_project: + description: + - The Google Cloud project ID to use for this invocation. + - If omitted, then the current active project is assumed. + type: string + vars: + - name: ansible_gcloud_project + ini: + - section: gcloud + key: project + env: + - name: CLOUDSDK_CORE_PROJECT + gcloud_account: + description: + - Google cloud account to use for invocation. + type: string + vars: + - name: ansible_gcloud_account + ini: + - section: gcloud + key: account + env: + - name: CLOUDSDK_CORE_ACCOUNT + gcloud_zone: + description: + - The Google Cloud zone to use for the instance(s). + type: string + vars: + - name: ansible_gcloud_zone + ini: + - section: gcloud + key: zone + env: + - name: CLOUDSDK_COMPUTE_ZONE + gcloud_access_token_file: + description: + - A file to read the access token from. + - The credentials of the active account (if exists) will be ignored. + type: string + vars: + - name: ansible_access_token_file + ini: + - section: gcloud + key: access_token_file + env: + - name: CLOUDSDK_AUTH_ACCESS_TOKEN_FILE + host: + description: Google Cloud instance name to connect to. + default: inventory_hostname + type: string + vars: + - name: inventory_hostname + - name: ansible_host + - name: ansible_gcloud_host + known_hosts_file: + description: Path to the UserKnownHosts file storing SSH fingerprints. Defaults + to the same file used by `gcloud compute ssh` + type: string + default: ~/.ssh/google_compute_known_hosts + ini: + - section: ssh_connection + key: known_hosts_file + vars: + - name: ansible_known_hosts_file + - name: ansible_ssh_known_hosts_file + host_key_checking: + description: Determines if SSH should reject or not a connection after checking + host keys. + default: false + type: boolean + ini: + - section: defaults + key: host_key_checking + - section: ssh_connection + key: host_key_checking + env: + - name: ANSIBLE_HOST_KEY_CHECKING + - name: ANSIBLE_SSH_HOST_KEY_CHECKING + vars: + - name: ansible_host_key_checking + - name: ansible_ssh_host_key_checking + password: + description: + - Authentication password for the O(remote_user). Can be supplied as CLI option. + type: string + vars: + - name: ansible_password + - name: ansible_ssh_pass + - name: ansible_ssh_password + password_mechanism: + description: Mechanism to use for handling ssh password prompt + type: string + default: ssh_askpass + choices: + - ssh_askpass + - sshpass + - disable + env: + - name: ANSIBLE_SSH_PASSWORD_MECHANISM + ini: + - section: ssh_connection + key: password_mechanism + vars: + - name: ansible_ssh_password_mechanism + sshpass_prompt: + description: + - Password prompt that C(sshpass)/C(SSH_ASKPASS) should search for. + - Supported by sshpass 1.06 and up when O(password_mechanism) set to V(sshpass). + - Defaults to C(Enter PIN for) when pkcs11_provider is set. + - Defaults to C(assword) when O(password_mechanism) set to V(ssh_askpass). + default: '' + type: string + ini: + - section: ssh_connection + key: 'sshpass_prompt' + env: + - name: ANSIBLE_SSHPASS_PROMPT + vars: + - name: ansible_sshpass_prompt + ssh_args: + description: arguments to pass to all ssh cli tools. + type: string + default: '-C -o controlmaster=auto -o controlpersist=60s' + ini: + - section: ssh_connection + key: ssh_args + env: + - name: ansible_ssh_args + vars: + - name: ansible_ssh_args + ssh_common_args: + description: common extra args for all ssh cli tools. + type: string + default: '' + ini: + - section: ssh_connection + key: ssh_common_args + env: + - name: ANSIBLE_SSH_COMMON_ARGS + vars: + - name: ansible_ssh_common_args + cli: + - name: ssh_common_args + ssh_executable: + description: + - This defines the location of the SSH binary. It defaults to V(ssh) which will + use the first SSH binary available in $PATH. + - This option is usually not required, it might be useful when access to system + SSH is restricted, or when using SSH wrappers to connect to remote hosts. + type: string + default: ssh + ini: + - key: ssh_executable + section: ssh_connection + env: + - name: ANSIBLE_SSH_EXECUTABLE + vars: + - name: ansible_ssh_executable + sftp_executable: + description: + - This defines the location of the sftp binary. It defaults to V(sftp) which + will use the first binary available in $PATH. + type: string + default: sftp + ini: + - key: sftp_executable + section: ssh_connection + env: + - name: ANSIBLE_SFTP_EXECUTABLE + vars: + - name: ansible_sftp_executable + scp_executable: + description: + - This defines the location of the scp binary. It defaults to V(scp) which will + use the first binary available in $PATH. + type: string + default: scp + ini: + - section: ssh_connection + key: scp_executable + env: + - name: ANSIBLE_SCP_EXECUTABLE + vars: + - name: ansible_scp_executable + scp_extra_args: + description: Extra exclusive to the C(scp) CLI + type: string + default: '' + ini: + - section: ssh_connection + key: scp_extra_args + env: + - name: ANSIBLE_SCP_EXTRA_ARGS + vars: + - name: ansible_scp_extra_args + cli: + - name: scp_extra_args + sftp_extra_args: + description: Extra exclusive to the C(sftp) CLI + type: string + default: '' + ini: + - section: ssh_connection + key: sftp_extra_args + vars: + - name: ansible_sftp_extra_args + env: + - name: ANSIBLE_SFTP_EXTRA_ARGS + cli: + - name: sftp_extra_args + ssh_extra_args: + description: Extra exclusive to the SSH CLI. + type: string + default: '' + vars: + - name: ansible_ssh_extra_args + env: + - name: ANSIBLE_SSH_EXTRA_ARGS + ini: + - section: ssh_connection + key: ssh_extra_args + cli: + - name: ssh_extra_args + reconnection_retries: + description: + - Number of attempts to connect. + - Ansible retries connections only if it gets an SSH error with a return code + of 255. + - Any errors with return codes other than 255 indicate an issue with program + execution. + type: integer + default: 0 + env: + - name: ANSIBLE_SSH_RETRIES + ini: + - section: connection + key: retries + - section: ssh_connection + key: retries + vars: + - name: ansible_ssh_retries + port: + description: Remote port to connect to. + type: int + ini: + - section: defaults + key: remote_port + env: + - name: ANSIBLE_REMOTE_PORT + vars: + - name: ansible_port + - name: ansible_ssh_port + keyword: + - name: port + remote_user: + description: + - User name with which to login to the remote server, normally set by the + remote_user keyword. + - If no user is supplied, Ansible will let the SSH client binary choose the + user as it normally. + type: string + ini: + - section: defaults + key: remote_user + env: + - name: ANSIBLE_REMOTE_USER + vars: + - name: ansible_user + - name: ansible_ssh_user + cli: + - name: user + keyword: + - name: remote_user + private_key_file: + description: + - Path to private key file to use for authentication. + type: string + default: '~/.ssh/google_compute_engine' + ini: + - section: defaults + key: private_key_file + - section: gcloud + key: private_key_file + env: + - name: ANSIBLE_PRIVATE_KEY_FILE + vars: + - name: ansible_private_key_file + - name: ansible_ssh_private_key_file + - name: ansible_gcloud_private_key_file + cli: + - name: private_key_file + option: '--private-key' + private_key: + description: + - Private key contents in PEM format. Requires the C(SSH_AGENT) configuration + to be enabled. + type: string + env: + - name: ANSIBLE_PRIVATE_KEY + vars: + - name: ansible_private_key + - name: ansible_ssh_private_key + private_key_passphrase: + description: + - Private key passphrase, dependent on O(private_key). + - This does NOT have any effect when used with O(private_key_file). + type: string + env: + - name: ANSIBLE_PRIVATE_KEY_PASSPHRASE + vars: + - name: ansible_private_key_passphrase + - name: ansible_ssh_private_key_passphrase + control_path: + description: + - This is the location to save SSH's ControlPath sockets, it uses SSH's variable + substitution. + - Be aware that this setting is ignored if C(-o ControlPath) is set in ssh args. + type: string + env: + - name: ANSIBLE_SSH_CONTROL_PATH + ini: + - section: ssh_connection + key: control_path + vars: + - name: ansible_control_path + control_path_dir: + default: ~/.ansible/cp + description: + - This sets the directory to use for ssh control path if the control path + setting is null. + - Also, provides the ``%(directory)s`` variable for the control path setting. + type: string + env: + - name: ANSIBLE_SSH_CONTROL_PATH_DIR + ini: + - section: ssh_connection + key: control_path_dir + vars: + - name: ansible_control_path_dir + sftp_batch_mode: + description: + - When set to C(True), sftp will be run in batch mode, allowing detection of + transfer errors. + - When set to C(False), sftp will not be run in batch mode, preventing detection + of transfer errors. + type: bool + default: true + env: + - name: ANSIBLE_SFTP_BATCH_MODE + ini: + - section: ssh_connection + key: sftp_batch_mode + vars: + - name: ansible_sftp_batch_mode + ssh_transfer_method: + description: Preferred method to use when transferring files over ssh + type: string + default: smart + choices: + sftp: This is the most reliable way to copy things with SSH. + scp: Deprecated in OpenSSH. For OpenSSH >=9.0 you must add an additional option + to enable scp C(scp_extra_args="-O"). + piped: Creates an SSH pipe with C(dd) on either side to copy the data. + smart: Tries each method in order (sftp > scp > piped), until one succeeds or + they all fail. + env: + - name: ANSIBLE_SSH_TRANSFER_METHOD + ini: + - section: ssh_connection + key: transfer_method + vars: + - name: ansible_ssh_transfer_method + use_tty: + description: add -tt to ssh commands to force tty allocation. + type: bool + default: true + env: + - name: ANSIBLE_SSH_USETTY + ini: + - section: ssh_connection + key: usetty + vars: + - name: ansible_ssh_use_tty + timeout: + description: + - This is the default amount of time we will wait while establishing an SSH + connection. + - It also controls how long we can wait to access reading the connection once + established (select on the socket). + type: integer + default: 10 + env: + - name: ANSIBLE_TIMEOUT + - name: ANSIBLE_SSH_TIMEOUT + ini: + - section: defaults + key: timeout + - section: ssh_connection + key: timeout + vars: + - name: ansible_ssh_timeout + cli: + - name: timeout + pkcs11_provider: + description: + - PKCS11 SmartCard provider such as opensc, e.g. /usr/local/lib/opensc-pkcs11.so + type: string + default: '' + env: + - name: ANSIBLE_PKCS11_PROVIDER + ini: + - section: ssh_connection + key: pkcs11_provider + vars: + - name: ansible_ssh_pkcs11_provider + verbosity: + description: + - Requested verbosity level for the SSH CLI. + default: 0 + type: int + env: + - name: ANSIBLE_SSH_VERBOSITY + ini: + - section: ssh_connection + key: verbosity + vars: + - name: ansible_ssh_verbosity +""" import os import os.path as ospath @@ -13,7 +496,6 @@ import shutil import subprocess import threading import time -import yaml import tempfile import typing as T @@ -21,81 +503,6 @@ import ansible.plugins.connection.ssh as sshconn import ansible.errors as errors import ansible.utils.display as display -_my_opts = { - "gcloud_executable": { - "description": [ - "Path to the gcloud executable, defaults to whatever is found in", - "the PATH environment variable", - ], - "type": "string", - "vars": [{"name": "ansible_gcloud_executable"}], - "ini": [ - {"section": "gcloud", "key": "executable"}, - ], - }, - "gcloud_configuration": { - "description": ["Path to the gcloud configuration file if non default"], - "type": "string", - "vars": [{"name": "ansible_gcloud_configuration"}], - "ini": [{"section": "gcloud", "key": "configuration"}], - "env": [{"name": "CLOUDSDK_ACTIVE_CONFIG_NAME"}], - }, - "gcloud_project": { - "description": [ - "The Google Cloud project ID to use for this invocation.", - "If omitted, then the current project is assumed", - ], - "type": "string", - "vars": [{"name": "ansible_gcloud_project"}], - "ini": [{"section": "gcloud", "key": "project"}], - "env": [{"name": "CLOUDSDK_CORE_PROJECT"}], - }, - "gcloud_account": { - "description": ["Google cloud account to use for invocation"], - "type": "string", - "vars": [{"name": "ansible_gcloud_account"}], - "ini": [{"section": "gcloud", "key": "account"}], - "env": [{"name": "CLOUDSDK_CORE_ACCOUNT"}], - }, - "gcloud_zone": { - "description": ["The Google Cloud zone to use for the instance(s)"], - "type": "string", - "vars": [{"name": "ansible_gcloud_zone"}], - "ini": [{"section": "gcloud", "key": "zone"}], - "env": [{"name": "CLOUDSDK_COMPUTE_ZONE"}], - }, - "gcloud_access_token_file": { - "description": [ - "A file to read the access token from. ", - "The credentials of the active account (if exists) will be ignored.", - ], - "type": "string", - "vars": [{"name": "ansible_access_token_file"}], - "ini": [{"section": "gcloud", "key": "access_token_file"}], - "env": [{"name": "CLOUDSDK_AUTH_ACCESS_TOKEN_FILE"}], - }, -} - -# piggy back on top of upstream SSH plugin's docs, this somehow breaks ansible-doc but -# it still works for other ansible commands. This is expensive but I don't want to -# duplicate the entire doc string from ssh.py -_doc = yaml.safe_load(sshconn.DOCUMENTATION) -_doc["name"] = "gcloud-iap" -_doc["short_description"] = "connect using Google Cloud's Identity Aware Proxy (IAP)" -_doc["description"] = [ - "This connection plugin behaves almost like the stock SSH plugin, ", - "but it creates a new IAP process per host in the inventory so connections ", - "are tunneled through it. ", - "This plugin requires you to have set authentication prior to using it.", -] -_doc["author"] = "Jorge A Gallegos (@kad)" -# Add custom opts for this plugin -_doc["options"].update(_my_opts) -# Change default to stock SSH key used by gcloud -_doc["options"]["private_key_file"]["default"] = "~/.ssh/google_compute_engine" - -DOCUMENTATION = yaml.dump(_doc) - D = display.Display() DEFAULT_GCLOUD: T.Optional[str] = shutil.which("gcloud") DEFAULT_SSH_PORT: int = 22 @@ -275,14 +682,6 @@ class Connection(sshconn.Connection): "no executable defined in ansible config" ) - # have to trick SSH to connect to localhost instead of the instances - fd, self.ssh_config = tempfile.mkstemp( - suffix="ssh_config", prefix="ansible_gcloud", text=True - ) - with open(fd, "w") as fp: - fp.write("Host *\n") - fp.write(" HostName localhost\n") - def _connect(self) -> Connection: """Upstream ssh is empty, overload with the stuff starting the IAP tunnel""" @@ -330,9 +729,22 @@ class Connection(sshconn.Connection): # override port with the random IAP port self.set_option("port", self.iaps[host].local_port) - # disable host_key_checking because it's impossible to know ports beforehand - self.set_option("host_key_checking", False) - # prepend our generated tiny ssh config to all ssh_args if not already present + + # read path to the supplied known hosts file + ukhf: str = ospath.abspath( + ospath.expanduser(str(self.get_option("known_hosts_file"))) + ) + # have to trick SSH to connect to localhost instead of the instances + fd, self.ssh_config = tempfile.mkstemp( + suffix="ssh_config", prefix="ansible_gcloud", text=True + ) + with open(fd, "w") as fp: + fp.write("Host *\n") + fp.write(" HostName localhost\n") # trick + fp.write(" HostKeyAlias {}\n".format(host)) # avoid multiple entries + fp.write(" UserKnownHostsFile {}\n".format(ukhf)) # as defined in opts + + # prepend our generated ssh config to all ssh_args if not already present if self.ssh_config not in str(self.get_option("ssh_args")): self.set_option( "ssh_args", f"-F {self.ssh_config} " + str(self.get_option("ssh_args"))