diff --git a/changelogs/fragments/114-use_ssh_client.yml b/changelogs/fragments/114-use_ssh_client.yml new file mode 100644 index 000000000..2150a1ea4 --- /dev/null +++ b/changelogs/fragments/114-use_ssh_client.yml @@ -0,0 +1,2 @@ +minor_changes: +- "Add the ``use_ssh_client`` option to most docker modules and plugins (https://github.com/ansible-collections/community.docker/issues/108, https://github.com/ansible-collections/community.docker/pull/114)." diff --git a/plugins/doc_fragments/docker.py b/plugins/doc_fragments/docker.py index f0d06e64e..f3006e8ec 100644 --- a/plugins/doc_fragments/docker.py +++ b/plugins/doc_fragments/docker.py @@ -80,6 +80,13 @@ class ModuleDocFragment(object): instead. If the environment variable is not set, the default value will be used. type: bool default: no + use_ssh_client: + description: + - For SSH transports, use the C(ssh) CLI tool instead of paramiko. + - Requires Docker SDK for Python 4.4.0 or newer. + type: bool + default: no + version_added: 1.5.0 validate_certs: description: - Secure the connection to the API by using TLS and verifying the authenticity of the Docker host server. diff --git a/plugins/inventory/docker_containers.py b/plugins/inventory/docker_containers.py index ef2697a65..dc7eb22ad 100644 --- a/plugins/inventory/docker_containers.py +++ b/plugins/inventory/docker_containers.py @@ -134,9 +134,7 @@ from ansible.errors import AnsibleError from ansible.module_utils._text import to_native -from ansible_collections.community.docker.plugins.module_utils.common import update_tls_hostname, get_connect_params from ansible.plugins.inventory import BaseInventoryPlugin, Constructable -from ansible.parsing.utils.addresses import parse_address from ansible_collections.community.docker.plugins.module_utils.common import ( RequestException, @@ -146,7 +144,7 @@ ) try: - from docker.errors import DockerException, APIError, NotFound + from docker.errors import DockerException, APIError except Exception: # missing Docker SDK for Python handled in ansible_collections.community.docker.plugins.module_utils.common pass diff --git a/plugins/inventory/docker_swarm.py b/plugins/inventory/docker_swarm.py index 65c5f719f..93b406b5f 100644 --- a/plugins/inventory/docker_swarm.py +++ b/plugins/inventory/docker_swarm.py @@ -86,6 +86,13 @@ type: int default: 60 aliases: [ time_out ] + use_ssh_client: + description: + - For SSH transports, use the C(ssh) CLI tool instead of paramiko. + - Requires Docker SDK for Python 4.4.0 or newer. + type: bool + default: no + version_added: 1.5.0 include_host_uri: description: Toggle to return the additional attribute C(ansible_host_uri) which contains the URI of the swarm leader in format of C(tcp://172.16.0.1:2376). This value may be used without additional @@ -171,6 +178,7 @@ def _populate(self): api_version=self.get_option('api_version'), timeout=self.get_option('timeout'), ssl_version=self.get_option('ssl_version'), + use_ssh_client=self.get_option('use_ssh_client'), debug=None, ) update_tls_hostname(raw_params) diff --git a/plugins/module_utils/common.py b/plugins/module_utils/common.py index e2a799187..e364c3e26 100644 --- a/plugins/module_utils/common.py +++ b/plugins/module_utils/common.py @@ -91,6 +91,7 @@ class RequestException(Exception): client_key=dict(type='path', aliases=['tls_client_key', 'key_path']), ssl_version=dict(type='str', fallback=(env_fallback, ['DOCKER_SSL_VERSION'])), tls=dict(type='bool', default=DEFAULT_TLS, fallback=(env_fallback, ['DOCKER_TLS'])), + use_ssh_client=dict(type='bool', default=False), validate_certs=dict(type='bool', default=DEFAULT_TLS_VERIFY, fallback=(env_fallback, ['DOCKER_TLS_VERIFY']), aliases=['tls_verify']), debug=dict(type='bool', default=False) ) @@ -193,75 +194,43 @@ def get_connect_params(auth, fail_function): if auth['tls'] or auth['tls_verify']: auth['docker_host'] = auth['docker_host'].replace('tcp://', 'https://') - if auth['tls_verify'] and auth['cert_path'] and auth['key_path']: - # TLS with certs and host verification - if auth['cacert_path']: - tls_config = _get_tls_config(client_cert=(auth['cert_path'], auth['key_path']), - ca_cert=auth['cacert_path'], - verify=True, - assert_hostname=auth['tls_hostname'], - ssl_version=auth['ssl_version'], - fail_function=fail_function) - else: - tls_config = _get_tls_config(client_cert=(auth['cert_path'], auth['key_path']), - verify=True, - assert_hostname=auth['tls_hostname'], - ssl_version=auth['ssl_version'], - fail_function=fail_function) - - return dict(base_url=auth['docker_host'], - tls=tls_config, - version=auth['api_version'], - timeout=auth['timeout']) - - if auth['tls_verify'] and auth['cacert_path']: - # TLS with cacert only - tls_config = _get_tls_config(ca_cert=auth['cacert_path'], - assert_hostname=auth['tls_hostname'], - verify=True, - ssl_version=auth['ssl_version'], - fail_function=fail_function) - return dict(base_url=auth['docker_host'], - tls=tls_config, - version=auth['api_version'], - timeout=auth['timeout']) + result = dict( + base_url=auth['docker_host'], + version=auth['api_version'], + timeout=auth['timeout'], + ) if auth['tls_verify']: - # TLS with verify and no certs - tls_config = _get_tls_config(verify=True, - assert_hostname=auth['tls_hostname'], - ssl_version=auth['ssl_version'], - fail_function=fail_function) - return dict(base_url=auth['docker_host'], - tls=tls_config, - version=auth['api_version'], - timeout=auth['timeout']) - - if auth['tls'] and auth['cert_path'] and auth['key_path']: - # TLS with certs and no host verification - tls_config = _get_tls_config(client_cert=(auth['cert_path'], auth['key_path']), - verify=False, - ssl_version=auth['ssl_version'], - fail_function=fail_function) - return dict(base_url=auth['docker_host'], - tls=tls_config, - version=auth['api_version'], - timeout=auth['timeout']) - - if auth['tls']: - # TLS with no certs and not host verification - tls_config = _get_tls_config(verify=False, - ssl_version=auth['ssl_version'], - fail_function=fail_function) - return dict(base_url=auth['docker_host'], - tls=tls_config, - version=auth['api_version'], - timeout=auth['timeout']) + # TLS with verification + tls_config = dict( + verify=True, + assert_hostname=auth['tls_hostname'], + ssl_version=auth['ssl_version'], + fail_function=fail_function, + ) + if auth['cert_path'] and auth['key_path']: + tls_config['client_cert'] = (auth['cert_path'], auth['key_path']) + if auth['cacert_path']: + tls_config['ca_cert'] = auth['cacert_path'] + result['tls'] = _get_tls_config(**tls_config) + elif auth['tls']: + # TLS without verification + tls_config = dict( + verify=False, + ssl_version=auth['ssl_version'], + fail_function=fail_function, + ) + if auth['cert_path'] and auth['key_path']: + tls_config['client_cert'] = (auth['cert_path'], auth['key_path']) + result['tls'] = _get_tls_config(**tls_config) + + if auth.get('use_ssh_client'): + if LooseVersion(docker_version) < LooseVersion('4.4.0'): + fail_function("use_ssh_client=True requires Docker SDK for Python 4.4.0 or newer") + result['use_ssh_client'] = True # No TLS - return dict(base_url=auth['docker_host'], - version=auth['api_version'], - timeout=auth['timeout']) + return result DOCKERPYUPGRADE_SWITCH_TO_DOCKER = "Try `pip uninstall docker-py` followed by `pip install docker`." @@ -399,6 +368,7 @@ def auth_params(self): DEFAULT_TLS_VERIFY), timeout=self._get_value('timeout', params['timeout'], 'DOCKER_TIMEOUT', DEFAULT_TIMEOUT_SECONDS), + use_ssh_client=self._get_value('use_ssh_client', params['use_ssh_client'], None, False), ) update_tls_hostname(result) diff --git a/plugins/modules/docker_compose.py b/plugins/modules/docker_compose.py index e8b8532c6..823920186 100644 --- a/plugins/modules/docker_compose.py +++ b/plugins/modules/docker_compose.py @@ -143,13 +143,16 @@ default: no timeout: description: - - timeout in seconds for container shutdown when attached or when containers are already running. + - Timeout in seconds for container shutdown when attached or when containers are already running. type: int default: 10 + use_ssh_client: + description: + - Currently ignored for this module, but might suddenly be supported later on. extends_documentation_fragment: -- community.docker.docker -- community.docker.docker.docker_py_1_documentation + - community.docker.docker + - community.docker.docker.docker_py_1_documentation requirements: diff --git a/tests/integration/targets/generic_ssh_connection/aliases b/tests/integration/targets/generic_ssh_connection/aliases new file mode 100644 index 000000000..2642b1f63 --- /dev/null +++ b/tests/integration/targets/generic_ssh_connection/aliases @@ -0,0 +1,4 @@ +shippable/posix/group4 +destructive +needs/root +skip/docker # we need a VM, and not a container diff --git a/tests/integration/targets/generic_ssh_connection/meta/main.yml b/tests/integration/targets/generic_ssh_connection/meta/main.yml new file mode 100644 index 000000000..b69559bec --- /dev/null +++ b/tests/integration/targets/generic_ssh_connection/meta/main.yml @@ -0,0 +1,4 @@ +--- +dependencies: + - setup_docker + - setup_paramiko diff --git a/tests/integration/targets/generic_ssh_connection/tasks/main.yml b/tests/integration/targets/generic_ssh_connection/tasks/main.yml new file mode 100644 index 000000000..646367f0f --- /dev/null +++ b/tests/integration/targets/generic_ssh_connection/tasks/main.yml @@ -0,0 +1,77 @@ +--- +#################################################################### +# WARNING: These are designed specifically for Ansible tests # +# and should not be used as examples of how to write Ansible roles # +#################################################################### + +- name: Get docker daemon information directly + docker_host_info: + register: output + +- name: Make sure we got information + assert: + that: + - 'output.host_info.Name is string' + - 'output.containers is not defined' + - 'output.networks is not defined' + - 'output.volumes is not defined' + - 'output.images is not defined' + - 'output.disk_usage is not defined' + +- name: Show contents of ~/.ssh + command: ls -lah ~/.ssh + ignore_errors: true + +- name: Create SSH config + copy: + dest: "{{ lookup('env', 'HOME') }}/.ssh/config" + mode: '0600' + content: | + Host localhost + User root + IdentityFile ~/.ssh/id_rsa + +- name: Get docker daemon information via ssh (paramiko) to localhost + docker_host_info: + docker_host: "ssh://root@localhost" + register: output + ignore_errors: true + +- name: Make sure we got information + assert: + that: + - 'output.host_info.Name is string' + - 'output.containers is not defined' + - 'output.networks is not defined' + - 'output.volumes is not defined' + - 'output.images is not defined' + - 'output.disk_usage is not defined' + when: output is succeeded or 'Install paramiko package to enable' in output.msg + # For whatever reason, even though paramiko is installed, *sometimes* this error + # shows up. I have no idea why it sometimes works and sometimes not... + +- name: Get docker daemon information via ssh (OpenSSH) to localhost + docker_host_info: + docker_host: "ssh://root@localhost" + use_ssh_client: true + register: output + ignore_errors: true + +- name: Make sure we got information + assert: + that: + - output is succeeded + - 'output.host_info.Name is string' + - 'output.containers is not defined' + - 'output.networks is not defined' + - 'output.volumes is not defined' + - 'output.images is not defined' + - 'output.disk_usage is not defined' + when: docker_py_version is version('4.4.0', '>=') + +- name: Make sure we got information + assert: + that: + - output is failed + - "'use_ssh_client=True requires Docker SDK for Python 4.4.0 or newer' in output.msg" + when: docker_py_version is version('4.4.0', '<') diff --git a/tests/integration/targets/setup_paramiko/meta/main.yml b/tests/integration/targets/setup_paramiko/meta/main.yml new file mode 100644 index 000000000..d20e3de80 --- /dev/null +++ b/tests/integration/targets/setup_paramiko/meta/main.yml @@ -0,0 +1,4 @@ +--- +dependencies: + - setup_remote_constraints + - setup_openssl # so cryptography is installed diff --git a/tests/integration/targets/setup_paramiko/tasks/main.yml b/tests/integration/targets/setup_paramiko/tasks/main.yml new file mode 100644 index 000000000..41af930f2 --- /dev/null +++ b/tests/integration/targets/setup_paramiko/tasks/main.yml @@ -0,0 +1,6 @@ +--- +- name: Install paramiko + pip: + name: "paramiko{% if cryptography_version.stdout is version('2.5.0', '<') %}<2.5.0{% endif %}" + extra_args: "-c {{ remote_constraints }}" + become: true