diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index cc20f04a..15dda039 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -156,30 +156,21 @@ jobs: "$PYTHON" -m tox -e "${{ matrix.tox_env }}" macos: - # https://github.com/actions/runner-images/blob/main/images/macos/macos-12-Readme.md - runs-on: macos-12 + # https://github.com/actions/runner-images/blob/main/images/macos/macos-13-Readme.md + runs-on: macos-13 timeout-minutes: 120 strategy: fail-fast: false matrix: include: - - name: Mito_27 - tox_env: py27-mode_mitogen - name: Mito_313 - python_version: '3.13' tox_env: py313-mode_mitogen - - name: Loc_27_210 - tox_env: py27-mode_localhost-ansible2.10 - name: Loc_313_10 - python_version: '3.13' tox_env: py313-mode_localhost-ansible10 - - name: Van_27_210 - tox_env: py27-mode_localhost-ansible2.10-strategy_linear - name: Van_313_10 - python_version: '3.13' tox_env: py313-mode_localhost-ansible10-strategy_linear steps: diff --git a/ansible_mitogen/connection.py b/ansible_mitogen/connection.py index 4c1df1bd..1231acb5 100644 --- a/ansible_mitogen/connection.py +++ b/ansible_mitogen/connection.py @@ -890,6 +890,29 @@ class Connection(ansible.plugins.connection.ConnectionBase): self.binding.close() self.binding = None + def _mitogen_var_options(self, templar): + # Workaround for https://github.com/ansible/ansible/issues/84238 + var_names = C.config.get_plugin_vars('connection', self._load_name) + variables = templar.available_variables + var_options = { + var_name: templar.template(variables[var_name]) + for var_name in var_names + if var_name in variables + } + + if self.allow_extras: + extras_var_prefix = 'ansible_%s_' % self.extras_prefix + var_options['_extras'] = { + var_name: templar.template(variables[var_name]) + for var_name in variables + if var_name not in var_options + and var_name.startswith(extras_var_prefix) + } + else: + var_options['_extras'] = {} + + return var_options + reset_compat_msg = ( 'Mitogen only supports "reset_connection" on Ansible 2.5.6 or later' ) @@ -922,6 +945,19 @@ class Connection(ansible.plugins.connection.ConnectionBase): shared_loader_obj=0 ) + # Workaround for https://github.com/ansible/ansible/issues/84238 + try: + task, templar = self._play_context.vars.pop( + '_mitogen.smuggled.reset_connection', + ) + except KeyError: + pass + else: + self.set_options( + task_keys=task.dump_attrs(), + var_options=self._mitogen_var_options(templar), + ) + # Clear out state in case we were ever connected. self.close() diff --git a/ansible_mitogen/strategy.py b/ansible_mitogen/strategy.py index 0a98e316..c319f3e1 100644 --- a/ansible_mitogen/strategy.py +++ b/ansible_mitogen/strategy.py @@ -45,6 +45,7 @@ import ansible_mitogen.mixins import ansible_mitogen.process import ansible.executor.process.worker +import ansible.template import ansible.utils.sentinel @@ -326,3 +327,24 @@ class StrategyMixin(object): self._worker_model.on_strategy_complete() finally: ansible_mitogen.process.set_worker_model(None) + + def _smuggle_to_connction_reset(self, task, play_context, iterator, target_host): + # Workaround for https://github.com/ansible/ansible/issues/84238 + variables = self._variable_manager.get_vars( + play=iterator._play, host=target_host, task=task, + _hosts=self._hosts_cache, _hosts_all=self._hosts_cache_all, + ) + templar = ansible.template.Templar( + loader=self._loader, variables=variables, + ) + play_context.vars.update({ + '_mitogen.smuggled.reset_connection': (task, templar), + }) + + def _execute_meta(self, task, play_context, iterator, target_host): + if task.args['_raw_params'] == 'reset_connection': + self._smuggle_to_connction_reset(task, play_context, iterator, target_host) + + return super(StrategyMixin, self)._execute_meta( + task, play_context, iterator, target_host, + ) diff --git a/ansible_mitogen/transport_config.py b/ansible_mitogen/transport_config.py index c2976365..97f1b2f0 100644 --- a/ansible_mitogen/transport_config.py +++ b/ansible_mitogen/transport_config.py @@ -213,6 +213,12 @@ class Spec(with_metaclass(abc.ABCMeta, object)): :data:`True` if privilege escalation should be active. """ + @abc.abstractmethod + def become_flags(self): + """ + The command line arguments passed to the become executable. + """ + @abc.abstractmethod def become_method(self): """ @@ -290,10 +296,9 @@ class Spec(with_metaclass(abc.ABCMeta, object)): @abc.abstractmethod def sudo_args(self): """ - The list of additional arguments that should be included in a become + The list of additional arguments that should be included in a sudo invocation. """ - # TODO: split out into sudo_args/become_args. @abc.abstractmethod def mitogen_via(self): @@ -419,7 +424,29 @@ class PlayContextSpec(Spec): def _become_option(self, name): plugin = self._connection.become - return plugin.get_option(name, self._task_vars, self._play_context) + try: + return plugin.get_option(name, self._task_vars, self._play_context) + except AttributeError: + # A few ansible_mitogen connection plugins look more like become + # plugins. They don't quite fit Ansible's plugin.get_option() API. + # https://github.com/mitogen-hq/mitogen/issues/1173 + fallback_plugins = {'mitogen_doas', 'mitogen_sudo', 'mitogen_su'} + if self._connection.transport not in fallback_plugins: + raise + + fallback_options = { + 'become_exe', + 'become_flags', + } + if name not in fallback_options: + raise + + LOG.info( + 'Used PlayContext fallback for plugin=%r, option=%r', + self._connection, name, + ) + return getattr(self._play_context, name) + def _connection_option(self, name): try: @@ -443,6 +470,9 @@ class PlayContextSpec(Spec): def become(self): return self._connection.become + def become_flags(self): + return self._become_option('become_flags') + def become_method(self): return self._play_context.become_method @@ -481,7 +511,7 @@ class PlayContextSpec(Spec): return self._play_context.private_key_file def ssh_executable(self): - return C.config.get_config_value("ssh_executable", plugin_type="connection", plugin_name="ssh", variables=self._task_vars.get("vars", {})) + return self._connection_option('ssh_executable') def timeout(self): return self._play_context.timeout @@ -505,30 +535,10 @@ class PlayContextSpec(Spec): ] def become_exe(self): - # In Ansible 2.8, PlayContext.become_exe always has a default value due - # to the new options mechanism. Previously it was only set if a value - # ("somewhere") had been specified for the task. - # For consistency in the tests, here we make older Ansibles behave like - # newer Ansibles. - exe = self._play_context.become_exe - if exe is None and self._play_context.become_method == 'sudo': - exe = 'sudo' - return exe + return self._become_option('become_exe') def sudo_args(self): - return [ - mitogen.core.to_text(term) - for term in ansible.utils.shlex.shlex_split( - first_true(( - self._play_context.become_flags, - # Ansible <=2.7. - getattr(self._play_context, 'sudo_flags', ''), - # Ansible <=2.3. - getattr(C, 'DEFAULT_BECOME_FLAGS', ''), - getattr(C, 'DEFAULT_SUDO_FLAGS', '') - ), default='') - ) - ] + return ansible.utils.shlex.shlex_split(self.become_flags() or '') def mitogen_via(self): return self._connection.get_task_var('mitogen_via') @@ -663,6 +673,9 @@ class MitogenViaSpec(Spec): def become(self): return bool(self._become_user) + def become_flags(self): + return self._host_vars.get('ansible_become_flags') + def become_method(self): return ( self._become_method or @@ -758,7 +771,7 @@ class MitogenViaSpec(Spec): mitogen.core.to_text(term) for s in ( self._host_vars.get('ansible_sudo_flags') or '', - self._host_vars.get('ansible_become_flags') or '', + self.become_flags() or '', ) for term in ansible.utils.shlex.shlex_split(s) ] diff --git a/docs/changelog.rst b/docs/changelog.rst index 1880a9f5..aa910a91 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -18,6 +18,20 @@ To avail of fixes in an unreleased version, please download a ZIP file `directly from GitHub `_. +v0.3.16 (2024-11-05) +-------------------- + +* :gh:issue:`1083` :mod:`ansible_mitogen`: Templated become executable + (e.g. ``become_exe``). +* :gh:issue:`1083` :mod:`ansible_mitogen`: Templated become executable + arguments (e.g. ``become_flags``). +* :gh:issue:`1083` :mod:`ansible_mitogen`: Templated ssh executable + (``ansible_ssh_executable``). +* :gh:issue:`1083` :mod:`ansible_mitogen`: Fixed templated connection options + during a ``meta: reset_connection`` task. +* :gh:issue:`1129` CI: Migrated macOS 12 runners to macOS 13, due to EOL. + + v0.3.15 (2024-10-28) -------------------- diff --git a/mitogen/__init__.py b/mitogen/__init__.py index d0fee0db..50c2e377 100644 --- a/mitogen/__init__.py +++ b/mitogen/__init__.py @@ -35,7 +35,7 @@ be expected. On the slave, it is built dynamically during startup. #: Library version as a tuple. -__version__ = (0, 3, 15) +__version__ = (0, 3, 16) #: This is :data:`False` in slave contexts. Previously it was used to prevent diff --git a/tests/ansible/hosts/default.hosts b/tests/ansible/hosts/default.hosts index 58e003b0..eb04cf90 100644 --- a/tests/ansible/hosts/default.hosts +++ b/tests/ansible/hosts/default.hosts @@ -33,6 +33,8 @@ ansible_host=localhost ansible_user="{{ lookup('pipe', 'whoami') }}" [tt_become_by_inv] +tt-become-exe ansible_become=true ansible_become_exe="{{ 'sudo' | trim }}" ansible_become_user=root +tt-become-flags ansible_become=true ansible_become_flags="{{ '--set-home --stdin --non-interactive' | trim }}" ansible_become_user=root tt-become-pass ansible_become=true ansible_become_pass="{{ 'pw_required_password' | trim }}" ansible_become_user=mitogen__pw_required tt-become-user ansible_become=true ansible_become_user="{{ 'root' | trim }}" @@ -44,6 +46,7 @@ ansible_user="{{ lookup('pipe', 'whoami') }}" tt-password ansible_password="{{ 'has_sudo_nopw_password' | trim }}" ansible_user=mitogen__has_sudo_nopw tt-port ansible_password=has_sudo_nopw_password ansible_port="{{ 22 | int }}" ansible_user=mitogen__has_sudo_nopw tt-remote-user ansible_password=has_sudo_nopw_password ansible_user="{{ 'mitogen__has_sudo_nopw' | trim }}" +tt-ssh-executable ansible_password=has_sudo_nopw_password ansible_ssh_executable="{{ 'ssh' | trim }}" ansible_user=mitogen__has_sudo_nopw [tt_targets_inventory:vars] ansible_host=localhost diff --git a/tests/ansible/integration/become/templated_by_inv.yml b/tests/ansible/integration/become/templated_by_inv.yml index 3409708b..a65fcf7b 100644 --- a/tests/ansible/integration/become/templated_by_inv.yml +++ b/tests/ansible/integration/become/templated_by_inv.yml @@ -12,6 +12,8 @@ - name: Templated become in inventory vars: expected_become_users: + tt-become-exe: root + tt-become-flags: root tt-become-pass: mitogen__pw_required tt-become-user: root command: diff --git a/tests/ansible/integration/become/templated_by_play_keywords.yml b/tests/ansible/integration/become/templated_by_play_keywords.yml index 94d52726..67d06125 100644 --- a/tests/ansible/integration/become/templated_by_play_keywords.yml +++ b/tests/ansible/integration/become/templated_by_play_keywords.yml @@ -2,6 +2,8 @@ hosts: tt_become_bare gather_facts: false become: true + become_exe: "{{ 'sudo' | trim }}" + become_flags: "{{ '--set-home --stdin --non-interactive' | trim }}" become_user: "{{ 'root' | trim }}" tasks: - meta: reset_connection @@ -20,6 +22,8 @@ hosts: tt_become_bare gather_facts: false become: true + become_exe: "{{ 'sudo' | trim }}" + become_flags: "{{ '--set-home --stdin --non-interactive' | trim }}" become_user: "{{ 'mitogen__pw_required' | trim }}" vars: ansible_become_pass: "{{ 'pw_required_password' | trim }}" diff --git a/tests/ansible/integration/become/templated_by_play_vars.yml b/tests/ansible/integration/become/templated_by_play_vars.yml index c46ca144..cdfb8221 100644 --- a/tests/ansible/integration/become/templated_by_play_vars.yml +++ b/tests/ansible/integration/become/templated_by_play_vars.yml @@ -3,6 +3,8 @@ gather_facts: false vars: ansible_become: true + ansible_become_exe: "{{ 'sudo' | trim }}" + ansible_become_flags: "{{ '--set-home --stdin --non-interactive' | trim }}" ansible_become_user: "{{ 'root' | trim }}" tasks: - name: Templated become by play vars, no password @@ -20,6 +22,8 @@ gather_facts: false vars: ansible_become: true + ansible_become_exe: "{{ 'sudo' | trim }}" + ansible_become_flags: "{{ '--set-home --stdin --non-interactive' | trim }}" ansible_become_pass: "{{ 'pw_required_password' | trim }}" ansible_become_user: "{{ 'mitogen__pw_required' | trim }}" tasks: diff --git a/tests/ansible/integration/become/templated_by_task_keywords.yml b/tests/ansible/integration/become/templated_by_task_keywords.yml index 9c75cbd7..baea51e7 100644 --- a/tests/ansible/integration/become/templated_by_task_keywords.yml +++ b/tests/ansible/integration/become/templated_by_task_keywords.yml @@ -4,6 +4,8 @@ # FIXME Resetting the connection shouldn't require credentials # https://github.com/mitogen-hq/mitogen/issues/1132 become: true + become_exe: "{{ 'sudo' | trim }}" + become_flags: "{{ '--set-home --stdin --non-interactive' | trim }}" become_user: "{{ 'root' | trim }}" tasks: - name: Reset connection to target that will be delegate_to @@ -15,6 +17,8 @@ tasks: - name: Templated become by task keywords, with delegate_to become: true + become_exe: "{{ 'sudo' | trim }}" + become_flags: "{{ '--set-home --stdin --non-interactive' | trim }}" become_user: "{{ 'root' | trim }}" delegate_to: "{{ groups.tt_become_bare[0] }}" command: @@ -33,6 +37,8 @@ # FIXME Resetting the connection shouldn't require credentials # https://github.com/mitogen-hq/mitogen/issues/1132 become: true + become_exe: "{{ 'sudo' | trim }}" + become_flags: "{{ '--set-home --stdin --non-interactive' | trim }}" become_user: "{{ 'mitogen__pw_required' | trim }}" vars: ansible_become_pass: "{{ 'pw_required_password' | trim }}" @@ -52,6 +58,8 @@ - name: Templated become by task keywords, with delegate_to become: true + become_exe: "{{ 'sudo' | trim }}" + become_flags: "{{ '--set-home --stdin --non-interactive' | trim }}" become_user: "{{ 'mitogen__pw_required' | trim }}" delegate_to: "{{ groups.tt_become_bare[0] }}" vars: diff --git a/tests/ansible/integration/ssh/templated_by_play_taskvar.yml b/tests/ansible/integration/ssh/templated_by_play_taskvar.yml index fd4bc848..0662adcd 100644 --- a/tests/ansible/integration/ssh/templated_by_play_taskvar.yml +++ b/tests/ansible/integration/ssh/templated_by_play_taskvar.yml @@ -4,6 +4,7 @@ vars: ansible_password: "{{ 'has_sudo_nopw_password' | trim }}" ansible_port: "{{ hostvars[groups['test-targets'][0]].ansible_port | default(22) }}" + ansible_ssh_executable: "{{ 'ssh' | trim }}" ansible_user: "{{ 'mitogen__has_sudo_nopw' | trim }}" tasks: 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 4972d91a..a1f39f66 100644 --- a/tests/ansible/regression/issue_655__wait_for_connection_error.yml +++ b/tests/ansible/regression/issue_655__wait_for_connection_error.yml @@ -11,11 +11,16 @@ tasks: - meta: end_play when: - # TODO CI currently runs on macOS 12 & which isn't supported by Podman - # version available in Homebrew. + # Podman versions available in Homebrew have dropped macOS 12 support. - ansible_facts.system == 'Darwin' - ansible_facts.distribution_version is version('13.0', '<', strict=True) + - meta: end_play + when: + # Ansible 10 (ansible-core 2.17+) require Python 3.7+ on targets. + # On CentOS 8 /usr/libexec/platform-python is Python 3.6 + - ansible_version.full is version('2.17', '>=', strict=True) + - name: set up test container and run tests inside it block: - name: install deps @@ -33,6 +38,7 @@ - cmd: podman info timeout: 300 register: podman_machine + changed_when: true - debug: var: podman_machine @@ -41,11 +47,13 @@ - name: create container command: cmd: podman run --name testMitogen -d --rm centos:8 bash -c "sleep infinity & wait" + changed_when: true - name: add container to inventory add_host: name: testMitogen ansible_connection: podman + ansible_python_interpreter: /usr/libexec/platform-python # Python 3.6 ansible_user: root changed_when: false environment: @@ -57,6 +65,7 @@ - name: create test file file: path: /var/run/reboot-required + mode: u=rw,go=r state: touch - name: Check if reboot is required @@ -68,13 +77,16 @@ shell: sleep 2 && shutdown -r now "Ansible updates triggered" async: 1 poll: 0 - when: reboot_required.stat.exists == True + changed_when: true + when: + - reboot_required.stat.exists - name: Wait 300 seconds for server to become available wait_for_connection: delay: 30 timeout: 300 - when: reboot_required.stat.exists == True + when: + - reboot_required.stat.exists - name: cleanup test file file: @@ -90,6 +102,7 @@ loop: - cmd: podman stop testMitogen - cmd: podman machine stop + changed_when: true when: - ansible_facts.pkg_mgr in ['homebrew'] tags: diff --git a/tests/ansible/templates/test-targets.j2 b/tests/ansible/templates/test-targets.j2 index 2eeebef7..27949758 100644 --- a/tests/ansible/templates/test-targets.j2 +++ b/tests/ansible/templates/test-targets.j2 @@ -58,6 +58,8 @@ ansible_python_interpreter={{ tt.python_path }} ansible_user=mitogen__has_sudo_nopw [tt_become_by_inv] +tt-become-exe ansible_become=true ansible_become_exe="{{ '{{' }} 'sudo' | trim {{ '}}' }}" ansible_become_user=root +tt-become-flags ansible_become=true ansible_become_flags="{{ '{{' }} '--set-home --stdin --non-interactive' | trim {{ '}}' }}" ansible_become_user=root tt-become-pass ansible_become=true ansible_become_pass="{{ '{{' }} 'pw_required_password' | trim {{ '}}' }}" ansible_become_user=mitogen__pw_required tt-become-user ansible_become=true ansible_become_user="{{ '{{' }} 'root' | trim {{ '}}' }}" @@ -72,6 +74,7 @@ ansible_user=mitogen__has_sudo_nopw tt-password ansible_password="{{ '{{' }} 'has_sudo_nopw_password' | trim {{ '}}' }}" ansible_port={{ tt.port }} ansible_user=mitogen__has_sudo_nopw tt-port ansible_password=has_sudo_nopw_password ansible_port="{{ '{{' }} {{ tt.port }} | int {{ '}}' }}" ansible_user=mitogen__has_sudo_nopw tt-remote-user ansible_password=has_sudo_nopw_password ansible_port={{ tt.port }} ansible_user="{{ '{{' }} 'mitogen__has_sudo_nopw' | trim {{ '}}' }}" +tt-ssh-executable ansible_password=has_sudo_nopw_password ansible_port={{ tt.port }} ansible_ssh_executable="{{ '{{' }} 'ssh' | trim {{ '}}' }}" ansible_user=mitogen__has_sudo_nopw [tt_targets_inventory:vars] ansible_host={{ tt.hostname }}