diff --git a/.ci/localhost_ansible_tests.py b/.ci/localhost_ansible_tests.py index c50ef220a..c36bad269 100755 --- a/.ci/localhost_ansible_tests.py +++ b/.ci/localhost_ansible_tests.py @@ -93,5 +93,5 @@ with ci_lib.Fold('ansible'): os.chdir(TESTS_DIR) playbook = os.environ.get('PLAYBOOK', 'all.yml') - ci_lib.run('./run_ansible_playbook.py %s -l target %s', + ci_lib.run('./run_ansible_playbook.py %s %s', playbook, ' '.join(sys.argv[1:])) diff --git a/ansible_mitogen/connection.py b/ansible_mitogen/connection.py index dfc3aec40..6bdf11baf 100644 --- a/ansible_mitogen/connection.py +++ b/ansible_mitogen/connection.py @@ -119,7 +119,7 @@ def _connect_ssh(spec): """ Return ContextService arguments for an SSH connection. """ - if C.HOST_KEY_CHECKING: + if spec.host_key_checking(): check_host_keys = 'enforce' else: check_host_keys = 'ignore' diff --git a/ansible_mitogen/transport_config.py b/ansible_mitogen/transport_config.py index 1fc1e80a1..3ab623f89 100644 --- a/ansible_mitogen/transport_config.py +++ b/ansible_mitogen/transport_config.py @@ -67,6 +67,7 @@ import ansible.constants as C from ansible.module_utils.six import with_metaclass +from ansible.module_utils.parsing.convert_bool import boolean # this was added in Ansible >= 2.8.0; fallback to the default interpreter if necessary try: @@ -245,6 +246,12 @@ def python_path(self): Path to the Python interpreter on the target machine. """ + @abc.abstractmethod + def host_key_checking(self): + """ + Whether or not to check the keys of the target machine + """ + @abc.abstractmethod def private_key_file(self): """ @@ -466,6 +473,14 @@ def python_path(self, rediscover_python=False): action=self._action, rediscover_python=rediscover_python) + def host_key_checking(self): + def candidates(): + yield self._connection.get_task_var('ansible_ssh_host_key_checking') + yield self._connection.get_task_var('ansible_host_key_checking') + yield C.HOST_KEY_CHECKING + val = next((v for v in candidates() if v is not None), True) + return boolean(val) + def private_key_file(self): return self._play_context.private_key_file @@ -692,6 +707,14 @@ def python_path(self, rediscover_python=False): action=self._action, rediscover_python=rediscover_python) + def host_key_checking(self): + def candidates(): + yield self._host_vars.get('ansible_ssh_host_key_checking') + yield self._host_vars.get('ansible_host_key_checking') + yield C.HOST_KEY_CHECKING + val = next((v for v in candidates() if v is not None), True) + return boolean(val) + def private_key_file(self): # TODO: must come from PlayContext too. return ( diff --git a/docs/changelog.rst b/docs/changelog.rst index 407a8c78e..5d77910e8 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -24,6 +24,7 @@ Unreleased * :gh:issue:`952` Fix Ansible `--ask-become-pass`, add test coverage * :gh:issue:`957` Fix Ansible exception when executing against 10s of hosts "ValueError: filedescriptor out of range in select()" +* :gh:issue:`1066` Support Ansible `ansible_host_key_checking` & `ansible_ssh_host_key_checking` v0.3.7 (2024-04-08) diff --git a/tests/ansible/ansible.cfg b/tests/ansible/ansible.cfg index c34dd2193..537b50590 100644 --- a/tests/ansible/ansible.cfg +++ b/tests/ansible/ansible.cfg @@ -1,9 +1,12 @@ [defaults] any_errors_fatal = true -# callback_whitelist naming will be deprecated in ansible-core >= 2.15. -# callbacks_enabled naming was added in ansible-core 2.11 +# callbacks_enabled was added in Ansible 4 (ansible-core 2.11). # profile_tasks: Displays timing for each task and summary table of top N tasks # timer: Displays "Playbook run took 0 days, 0 hours, ..." +callbacks_enabled = + profile_tasks, + timer +# callback_whitelist was deprecated in Ansible >= 8 (ansible-core >= 2.15). callback_whitelist = profile_tasks, timer @@ -37,7 +40,9 @@ no_target_syslog = True # Required by integration/ssh/timeouts.yml timeout = 30 -# On Travis, paramiko check fails due to host key checking enabled. +# Ideally this would be true here and and overridden for hosts/groups. However +# ansible_host_key_checking don't work on Vanilla Ansible 2.10, even for +# static inventory hosts (ansible/ansible#49254, ansible/ansible#73708) host_key_checking = False [inventory] diff --git a/tests/ansible/hosts/transport_config.hosts b/tests/ansible/hosts/transport_config.hosts index 1c1c2e103..5d5c38346 100644 --- a/tests/ansible/hosts/transport_config.hosts +++ b/tests/ansible/hosts/transport_config.hosts @@ -13,6 +13,7 @@ tc_become tc_become_method tc_become_pass tc_become_user +tc_host_key_checking tc_password tc_port tc_remote_addr @@ -74,6 +75,11 @@ tc-become-pass-password ansible_become_password=apassword tc-become-pass-pass ansible_become_pass=apass tc-become-pass-both ansible_become_pass=bpass ansible_become_password=bpassword +[tc_host_key_checking] +tc-hkc-unset +tc-hkc-host-key-checking ansible_host_key_checking=true +tc-hkc-ssh-host-key-checking ansible_ssh_host_key_checking=true + [tc_port] tc-port-unset tc-port-explicit-port ansible_port=1234 diff --git a/tests/ansible/integration/transport_config/all.yml b/tests/ansible/integration/transport_config/all.yml index 548e7f7e2..b486549ba 100644 --- a/tests/ansible/integration/transport_config/all.yml +++ b/tests/ansible/integration/transport_config/all.yml @@ -2,6 +2,7 @@ - import_playbook: become_pass.yml - import_playbook: become_user.yml - import_playbook: become.yml +- import_playbook: host_key_checking.yml - import_playbook: password.yml - import_playbook: port.yml - import_playbook: python_path.yml diff --git a/tests/ansible/integration/transport_config/host_key_checking.yml b/tests/ansible/integration/transport_config/host_key_checking.yml new file mode 100644 index 000000000..b15e36b12 --- /dev/null +++ b/tests/ansible/integration/transport_config/host_key_checking.yml @@ -0,0 +1,94 @@ +# Each case is followed by mitogen_via= case to test hostvars method. + +- name: integration/transport_config/host_key_checking.yml + hosts: tc-hkc-unset + tasks: + - include_tasks: ../_mitogen_only.yml + - {mitogen_get_stack: {}, register: out} + - assert: + that: + - out.result | length == 1 + - out.result[0].method == "ssh" + - out.result[0].kwargs.check_host_keys == "ignore" + fail_msg: out={{ out }} + tags: + - mitogen_only + +- hosts: tc-hkc-unset + vars: + mitogen_via: tc-hkc-host-key-checking + tasks: + - include_tasks: ../_mitogen_only.yml + - {mitogen_get_stack: {}, register: out} + - assert: + that: + - out.result | length == 2 + - out.result[0].method == "ssh" + - out.result[0].kwargs.check_host_keys == "enforce" + - out.result[1].method == "ssh" + - out.result[1].kwargs.check_host_keys == "ignore" + fail_msg: out={{ out }} + tags: + - mitogen_only + + +- hosts: tc-hkc-host-key-checking + tasks: + - include_tasks: ../_mitogen_only.yml + - {mitogen_get_stack: {}, register: out} + - assert: + that: + - out.result | length == 1 + - out.result[0].method == "ssh" + - out.result[0].kwargs.check_host_keys == "enforce" + fail_msg: out={{ out }} + tags: + - mitogen_only + +- hosts: tc-hkc-host-key-checking + vars: + mitogen_via: tc-hkc-unset + tasks: + - include_tasks: ../_mitogen_only.yml + - {mitogen_get_stack: {}, register: out} + - assert: + that: + - out.result | length == 2 + - out.result[0].method == "ssh" + - out.result[0].kwargs.check_host_keys == "ignore" + - out.result[1].method == "ssh" + - out.result[1].kwargs.check_host_keys == "enforce" + fail_msg: out={{ out }} + tags: + - mitogen_only + + +- hosts: tc-hkc-ssh-host-key-checking + tasks: + - include_tasks: ../_mitogen_only.yml + - {mitogen_get_stack: {}, register: out} + - assert: + that: + - out.result | length == 1 + - out.result[0].method == "ssh" + - out.result[0].kwargs.check_host_keys == "enforce" + fail_msg: out={{ out }} + tags: + - mitogen_only + +- hosts: tc-hkc-ssh-host-key-checking + vars: + mitogen_via: tc-hkc-unset + tasks: + - include_tasks: ../_mitogen_only.yml + - {mitogen_get_stack: {}, register: out} + - assert: + that: + - out.result | length == 2 + - out.result[0].method == "ssh" + - out.result[0].kwargs.check_host_keys == "ignore" + - out.result[1].method == "ssh" + - out.result[1].kwargs.check_host_keys == "enforce" + fail_msg: out={{ out }} + tags: + - mitogen_only diff --git a/tests/ansible/regression/all.yml b/tests/ansible/regression/all.yml index 477caccd4..31541e9fc 100644 --- a/tests/ansible/regression/all.yml +++ b/tests/ansible/regression/all.yml @@ -15,3 +15,4 @@ - import_playbook: issue_655__wait_for_connection_error.yml - import_playbook: issue_776__load_plugins_called_twice.yml - import_playbook: issue_952__ask_become_pass.yml +- import_playbook: issue_1066__add_host__host_key_checking.yml diff --git a/tests/ansible/regression/issue_1066__add_host__host_key_checking.yml b/tests/ansible/regression/issue_1066__add_host__host_key_checking.yml new file mode 100644 index 000000000..4b4609b6e --- /dev/null +++ b/tests/ansible/regression/issue_1066__add_host__host_key_checking.yml @@ -0,0 +1,67 @@ +- name: regression/issue_1066__add_host__host_key_checking.yml + hosts: test-targets[0] + gather_facts: false + become: false + tasks: + - name: Add hosts dynamically + add_host: + name: "{{ item.name }}" + ansible_host_key_checking: "{{ item.host_key_checking | default(omit) }}" + ansible_ssh_host_key_checking: "{{ item.host_ssh_key_checking | default(omit) }}" + ansible_host: "{{ hostvars[inventory_hostname].ansible_host | default(omit) }}" + ansible_password: "{{ hostvars[inventory_hostname].ansible_password | default(omit) }}" + ansible_port: "{{ hostvars[inventory_hostname].ansible_port | default(omit) }}" + ansible_python_interpreter: "{{ hostvars[inventory_hostname].ansible_python_interpreter | default(omit) }}" + ansible_user: "{{ hostvars[inventory_hostname].ansible_user | default(omit) }}" + loop: + - {name: issue-1066-host-hkc-false, host_key_checking: false} + - {name: issue-1066-host-hkc-true, host_key_checking: true} + - {name: issue-1066-host-hskc-false, host_ssh_key_checking: false} + - {name: issue-1066-host-hskc-true, host_ssh_key_checking: true} + delegate_to: localhost + tags: + - issue_1066 + +- name: regression/issue_1066__add_host__host_key_checking.yml + hosts: issue-1066-host-* + gather_facts: false + become: false + serial: 1 + tasks: + - meta: reset_connection + + # The host key might be in ~/.ssh/known_hosts. If it's removed then no + # problem - test-targets hosts have host_key_checking=false. + - name: Remove existing host keys + known_hosts: + name: "{{ ansible_host }}" + state: absent + delegate_to: localhost + + - name: Ping dynamically added hosts + ping: + ignore_errors: true + ignore_unreachable: true + register: issue_1066_ping + + - debug: + var: issue_1066_ping + + - name: Confirm dynamically added hosts are/are not reachable + vars: + expected: + issue-1066-host-hkc-false: {} + issue-1066-host-hkc-true: {unreachable: true} + issue-1066-host-hskc-false: {} + issue-1066-host-hskc-true: {unreachable: true} + assert: + that: + - issue_1066_ping.unreachable is defined == expected[inventory_hostname].unreachable is defined + - issue_1066_ping.unreachable | default(42) == expected[inventory_hostname].unreachable | default(42) + # ansible_host_key_checking don't work on Vanilla Ansible 2.10, even for + # static inventory hosts (ansible/ansible#49254, ansible/ansible#73708). + when: + - ansible_version.full is version('2.11', '>=', strict=True) + or is_mitogen + tags: + - issue_1066 diff --git a/tests/ansible/regression/issue_655__wait_for_connection_error.yml b/tests/ansible/regression/issue_655__wait_for_connection_error.yml index fbdb9f2b1..9ad42a106 100644 --- a/tests/ansible/regression/issue_655__wait_for_connection_error.yml +++ b/tests/ansible/regression/issue_655__wait_for_connection_error.yml @@ -4,11 +4,19 @@ # since things are ran on localhost; Azure DevOps loses connection and fails # TODO: do we want to install docker a different way to be able to do this for other tests too --- -- name: regression/issue_655_wait_for_connection_error.yml +- name: regression/issue_655__wait_for_connection_error.yml hosts: localhost gather_facts: yes become: no tasks: + - meta: end_play + when: + # TODO CI currently runs on macOS 11 images in Azure DevOps. MacOS 11 + # is no longer supported by homebrew, so the following install + # task fails. + - ansible_facts.system == 'Darwin' + - ansible_facts.distribution_major_version == '11' + - name: set up test container and run tests inside it block: - name: install deps diff --git a/tests/ansible/setup/report_controller.yml b/tests/ansible/setup/report_controller.yml index d0d5cc153..5e34fb176 100644 --- a/tests/ansible/setup/report_controller.yml +++ b/tests/ansible/setup/report_controller.yml @@ -1,5 +1,5 @@ - name: Report controller parameters - hosts: localhost + hosts: test-targets[0] gather_facts: false tasks: - debug: @@ -9,9 +9,11 @@ - $(groups): "{{ lookup('pipe', 'groups') }}" - $(pwd): "{{ lookup('pipe', 'pwd') }}" - $(whoami): "{{ lookup('pipe', 'whoami') }}" + - ansible_inventory_sources: "{{ ansible_inventory_sources | default('') }}" - ansible_run_tags: "{{ ansible_run_tags | default('') }}" - ansible_playbook_python: "{{ ansible_playbook_python | default('') }}" - ansible_skip_tags: "{{ ansible_skip_tags | default('') }}" - ansible_version.full: "{{ ansible_version.full | default('') }}" - is_mitogen: "{{ is_mitogen | default('') }}" - playbook_dir: "{{ playbook_dir | default('') }}" + delegate_to: localhost