Skip to content

Commit 99c5cec

Browse files
committed
Merge remote-tracking branch 'origin/dmw'
* origin/dmw: issue #633: skip test on older Ansibles. issue #633: update Changelog. issue #633: various task_vars fixes issue #633: handle meta: reset_connection when become is active issue #633: take inventory_hostname from task_vars docs: , -> : docs: use new manpage alias in one more place
2 parents 2b05f22 + 3023ab3 commit 99c5cec

File tree

6 files changed

+156
-64
lines changed

6 files changed

+156
-64
lines changed

ansible_mitogen/connection.py

Lines changed: 59 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,12 @@
5656

5757
LOG = logging.getLogger(__name__)
5858

59+
task_vars_msg = (
60+
'could not recover task_vars. This means some connection '
61+
'settings may erroneously be reset to their defaults. '
62+
'Please report a bug if you encounter this message.'
63+
)
64+
5965

6066
def get_remote_name(spec):
6167
"""
@@ -486,15 +492,9 @@ class Connection(ansible.plugins.connection.ConnectionBase):
486492
# the case of the synchronize module.
487493
#
488494

489-
#: Set to the host name as it appears in inventory by on_action_run().
490-
inventory_hostname = None
491-
492495
#: Set to task_vars by on_action_run().
493496
_task_vars = None
494497

495-
#: Set to 'hostvars' by on_action_run()
496-
host_vars = None
497-
498498
#: Set by on_action_run()
499499
delegate_to_hostname = None
500500

@@ -527,12 +527,10 @@ def on_action_run(self, task_vars, delegate_to_hostname, loader_basedir):
527527
:param str loader_basedir:
528528
Loader base directory; see :attr:`loader_basedir`.
529529
"""
530-
self.inventory_hostname = task_vars['inventory_hostname']
531530
self._task_vars = task_vars
532-
self.host_vars = task_vars['hostvars']
533531
self.delegate_to_hostname = delegate_to_hostname
534532
self.loader_basedir = loader_basedir
535-
self._mitogen_reset(mode='put')
533+
self._put_connection()
536534

537535
def _get_task_vars(self):
538536
"""
@@ -552,8 +550,10 @@ def _get_task_vars(self):
552550
for new connections to be constructed in addition to the preconstructed
553551
connection passed into any running action.
554552
"""
555-
f = sys._getframe()
553+
if self._task_vars is not None:
554+
return self._task_vars
556555

556+
f = sys._getframe()
557557
while f:
558558
if f.f_code.co_name == 'run':
559559
f_locals = f.f_locals
@@ -571,9 +571,23 @@ def _get_task_vars(self):
571571

572572
f = f.f_back
573573

574-
LOG.warning('could not recover task_vars. This means some connection '
575-
'settings may erroneously be reset to their defaults. '
576-
'Please report a bug if you encounter this message.')
574+
raise ansible.errors.AnsibleConnectionFailure(task_vars_msg)
575+
576+
def get_host_vars(self, inventory_hostname):
577+
"""
578+
Fetch the HostVars for a host.
579+
580+
:returns:
581+
Variables dictionary or :data:`None`.
582+
:raises ansible.errors.AnsibleConnectionFailure:
583+
Task vars unavailable.
584+
"""
585+
task_vars = self._get_task_vars()
586+
hostvars = task_vars.get('hostvars')
587+
if hostvars:
588+
return hostvars.get(inventory_hostname)
589+
590+
raise ansible.errors.AnsibleConnectionFailure(task_vars_msg)
577591

578592
def get_task_var(self, key, default=None):
579593
"""
@@ -586,17 +600,16 @@ def get_task_var(self, key, default=None):
586600
does not make sense to extract connection-related configuration for the
587601
delegated-to machine from them.
588602
"""
589-
task_vars = self._task_vars or self._get_task_vars()
590-
if task_vars is not None:
591-
if self.delegate_to_hostname is None:
603+
task_vars = self._get_task_vars()
604+
if self.delegate_to_hostname is None:
605+
if key in task_vars:
606+
return task_vars[key]
607+
else:
608+
delegated_vars = task_vars['ansible_delegated_vars']
609+
if self.delegate_to_hostname in delegated_vars:
610+
task_vars = delegated_vars[self.delegate_to_hostname]
592611
if key in task_vars:
593612
return task_vars[key]
594-
else:
595-
delegated_vars = task_vars['ansible_delegated_vars']
596-
if self.delegate_to_hostname in delegated_vars:
597-
task_vars = delegated_vars[self.delegate_to_hostname]
598-
if key in task_vars:
599-
return task_vars[key]
600613

601614
return default
602615

@@ -628,15 +641,15 @@ def _spec_from_via(self, proxied_inventory_name, via_spec):
628641

629642
# must use __contains__ to avoid a TypeError for a missing host on
630643
# Ansible 2.3.
631-
if self.host_vars is None or inventory_name not in self.host_vars:
644+
via_vars = self.get_host_vars(inventory_name)
645+
if via_vars is None:
632646
raise ansible.errors.AnsibleConnectionFailure(
633647
self.unknown_via_msg % (
634648
via_spec,
635649
proxied_inventory_name,
636650
)
637651
)
638652

639-
via_vars = self.host_vars[inventory_name]
640653
return ansible_mitogen.transport_config.MitogenViaSpec(
641654
inventory_name=inventory_name,
642655
play_context=self._play_context,
@@ -712,7 +725,7 @@ def _build_stack(self):
712725
connection=self,
713726
play_context=self._play_context,
714727
transport=self.transport,
715-
inventory_name=self.inventory_hostname,
728+
inventory_name=self.get_task_var('inventory_hostname'),
716729
)
717730
stack = self._stack_from_spec(spec)
718731
return spec.inventory_name(), stack
@@ -781,15 +794,11 @@ def _connect(self):
781794
self.binding = worker_model.get_binding(inventory_name)
782795
self._connect_stack(stack)
783796

784-
def _mitogen_reset(self, mode):
797+
def _put_connection(self):
785798
"""
786799
Forget everything we know about the connected context. This function
787800
cannot be called _reset() since that name is used as a public API by
788801
Ansible 2.4 wait_for_connection plug-in.
789-
790-
:param str mode:
791-
Name of ContextService method to use to discard the context, either
792-
'put' or 'reset'.
793802
"""
794803
if not self.context:
795804
return
@@ -798,7 +807,7 @@ def _mitogen_reset(self, mode):
798807
mitogen.service.call(
799808
call_context=self.binding.get_service_context(),
800809
service_name='ansible_mitogen.services.ContextService',
801-
method_name=mode,
810+
method_name='put',
802811
context=self.context
803812
)
804813

@@ -813,24 +822,11 @@ def close(self):
813822
gracefully shut down, and wait for shutdown to complete. Safe to call
814823
multiple times.
815824
"""
816-
self._mitogen_reset(mode='put')
825+
self._put_connection()
817826
if self.binding:
818827
self.binding.close()
819828
self.binding = None
820829

821-
def _reset_find_task_vars(self):
822-
"""
823-
Monsterous hack: since "meta: reset_connection" does not run from an
824-
action, we cannot capture task variables via :meth:`on_action_run`.
825-
Instead walk the parent frames searching for the `all_vars` local from
826-
StrategyBase._execute_meta(). If this fails, just leave task_vars
827-
unset, likely causing a subtly wrong configuration to be selected.
828-
"""
829-
frame = sys._getframe()
830-
while frame and not self._task_vars:
831-
self._task_vars = frame.f_locals.get('all_vars')
832-
frame = frame.f_back
833-
834830
reset_compat_msg = (
835831
'Mitogen only supports "reset_connection" on Ansible 2.5.6 or later'
836832
)
@@ -842,20 +838,31 @@ def reset(self):
842838
the 'disconnected' state, and informs ContextService the connection is
843839
bad somehow, and should be shut down and discarded.
844840
"""
845-
if self._task_vars is None:
846-
self._reset_find_task_vars()
847-
848841
if self._play_context.remote_addr is None:
849842
# <2.5.6 incorrectly populate PlayContext for reset_connection
850843
# https://github.com/ansible/ansible/issues/27520
851844
raise ansible.errors.AnsibleConnectionFailure(
852845
self.reset_compat_msg
853846
)
854847

855-
self._connect()
856-
self._mitogen_reset(mode='reset')
857-
self.binding.close()
858-
self.binding = None
848+
# Clear out state in case we were ever connected.
849+
self.close()
850+
851+
inventory_name, stack = self._build_stack()
852+
if self._play_context.become:
853+
stack = stack[:-1]
854+
855+
worker_model = ansible_mitogen.process.get_worker_model()
856+
binding = worker_model.get_binding(inventory_name)
857+
try:
858+
mitogen.service.call(
859+
call_context=binding.get_service_context(),
860+
service_name='ansible_mitogen.services.ContextService',
861+
method_name='reset',
862+
stack=mitogen.utils.cast(list(stack)),
863+
)
864+
finally:
865+
binding.close()
859866

860867
# Compatibility with Ansible 2.4 wait_for_connection plug-in.
861868
_reset = reset

ansible_mitogen/services.py

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -156,20 +156,41 @@ def __init__(self, *args, **kwargs):
156156

157157
@mitogen.service.expose(mitogen.service.AllowParents())
158158
@mitogen.service.arg_spec({
159-
'context': mitogen.core.Context
159+
'stack': list,
160160
})
161-
def reset(self, context):
161+
def reset(self, stack):
162162
"""
163163
Return a reference, forcing close and discard of the underlying
164164
connection. Used for 'meta: reset_connection' or when some other error
165165
is detected.
166+
167+
:returns:
168+
:data:`True` if a connection was found to discard, otherwise
169+
:data:`False`.
166170
"""
167-
LOG.debug('%r.reset(%r)', self, context)
168-
self._lock.acquire()
169-
try:
171+
LOG.debug('%r.reset(%r)', self, stack)
172+
173+
l = mitogen.core.Latch()
174+
context = None
175+
with self._lock:
176+
for i, spec in enumerate(stack):
177+
key = key_from_dict(via=context, **spec)
178+
response = self._response_by_key.get(key)
179+
if response is None:
180+
LOG.debug('%r: could not find connection to shut down; '
181+
'failed at hop %d', self, i)
182+
return False
183+
184+
context = response['context']
185+
186+
mitogen.core.listen(context, 'disconnect', l.put)
170187
self._shutdown_unlocked(context)
171-
finally:
172-
self._lock.release()
188+
189+
# The timeout below is to turn a hang into a crash in case there is any
190+
# possible race between 'disconnect' signal subscription, and the child
191+
# abruptly disconnecting.
192+
l.get(timeout=30.0)
193+
return True
173194

174195
@mitogen.service.expose(mitogen.service.AllowParents())
175196
@mitogen.service.arg_spec({

docs/changelog.rst

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,16 @@ v0.2.9 (unreleased)
2121
To avail of fixes in an unreleased version, please download a ZIP file
2222
`directly from GitHub <https://github.com/dw/mitogen/>`_.
2323

24-
*(no changes)*
24+
* :gh:issue:`633`: :ans:mod:`meta: reset_connection <meta>` could fail to reset
25+
a connection when ``become: true`` was set on the playbook.
26+
27+
28+
Thanks!
29+
~~~~~~~
30+
31+
Mitogen would not be possible without the support of users. A huge thanks for
32+
bug reports, testing, features and fixes in this release contributed by
33+
`Can Ozokur httpe://github.com/canozokur/>`_,
2534

2635

2736
v0.2.8 (2019-08-18)
@@ -41,7 +50,7 @@ Enhancements
4150
<https://docs.ansible.com/ansible/latest/reference_appendices/interpreter_discovery.html>`_
4251
are not yet handled.
4352

44-
* :gh:issue:`419`, :gh:issue:`470`, file descriptor usage is approximately
53+
* :gh:issue:`419`, :gh:issue:`470`: file descriptor usage is approximately
4554
halved, as it is no longer necessary to separately manage read and write
4655
sides to work around a design problem.
4756

@@ -80,7 +89,7 @@ Mitogen for Ansible
8089
~~~~~~~~~~~~~~~~~~~
8190

8291
* :gh:issue:`363`: fix an obscure race matching *Permission denied* errors from
83-
some versions of ``su`` running on heavily loaded machines.
92+
some versions of :linux:man1:`su` running on heavily loaded machines.
8493

8594
* :gh:issue:`410`: Uses of :linux:man7:`unix` sockets are replaced with
8695
traditional :linux:man7:`pipe` pairs when SELinux is detected, to work around
@@ -266,7 +275,7 @@ Thanks!
266275

267276
Mitogen would not be possible without the support of users. A huge thanks for
268277
bug reports, testing, features and fixes in this release contributed by
269-
`Andreas Hubert <https://github.com/peshay>`_.
278+
`Andreas Hubert <https://github.com/peshay>`_,
270279
`Anton Markelov <https://github.com/strangeman>`_,
271280
`Dan <https://github.com/dsgnr>`_,
272281
`Dave Cottlehuber <https://github.com/dch>`_,

tests/ansible/integration/connection/all.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@
88
- include: put_large_file.yml
99
- include: put_small_file.yml
1010
- include: reset.yml
11+
- include: reset_become.yml
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# issue #633: Connection.reset() should ignore "become", and apply to the login
2+
# account.
3+
4+
- hosts: test-targets
5+
become: true
6+
gather_facts: false
7+
tasks:
8+
- debug: msg="reset_become.yml skipped on Ansible<2.5.6"
9+
when: ansible_version.full < '2.5.6'
10+
11+
- meta: end_play
12+
when: ansible_version.full < '2.5.6'
13+
14+
- name: save pid of the become acct
15+
custom_python_detect_environment:
16+
register: become_acct
17+
18+
- name: save pid of the login acct
19+
become: false
20+
custom_python_detect_environment:
21+
register: login_acct
22+
23+
- name: ensure login != become
24+
assert:
25+
that:
26+
- become_acct.pid != login_acct.pid
27+
28+
- name: reset the connection
29+
meta: reset_connection
30+
31+
- name: save new pid of the become acct
32+
custom_python_detect_environment:
33+
register: new_become_acct
34+
35+
- name: ensure become_acct != new_become_acct
36+
assert:
37+
that:
38+
- become_acct.pid != new_become_acct.pid
39+
40+
- name: save new pid of login acct
41+
become: false
42+
custom_python_detect_environment:
43+
register: new_login_acct
44+
45+
- name: ensure login_acct != new_login_acct
46+
assert:
47+
that:
48+
- login_acct.pid != new_login_acct.pid

tests/ansible/tests/connection_test.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,13 @@ class ConnectionMixin(MuxProcessMixin):
4646

4747
def make_connection(self):
4848
play_context = ansible.playbook.play_context.PlayContext()
49-
return self.klass(play_context, new_stdin=False)
49+
conn = self.klass(play_context, new_stdin=False)
50+
conn.on_action_run(
51+
task_vars={},
52+
delegate_to_hostname=None,
53+
loader_basedir=None,
54+
)
55+
return conn
5056

5157
def wait_for_completion(self):
5258
# put_data() is asynchronous, must wait for operation to happen. Do

0 commit comments

Comments
 (0)