Repository: ansible/ansible

Issue:
meta: reset_connection not working
<!---
Verify first that your issue/request is not already reported on GitHub.
Also test if the latest release, and master branch are affected too.
-->

##### ISSUE TYPE
<!--- Pick one below and delete the rest: -->
 - Bug Report

##### COMPONENT NAME
<!--- Name of the module/plugin/task/feature -->
`meta` module's `reset_connection`

##### ANSIBLE VERSION
<!--- Paste verbatim output from “ansible --version” between quotes below -->
```
[vagrant@localhost infra-bootstrap]$ ansible --version
ansible 2.3.0.0
  config file = /etc/ansible/ansible.cfg
  configured module search path = Default w/o overrides
  python version = 2.7.5 (default, Sep  5 2016, 02:30:38) [GCC 4.8.5 20150623 (Red Hat 4.8.5-9)]
```

##### CONFIGURATION
<!---
Mention any settings you have changed/added/removed in ansible.cfg
(or using the ANSIBLE_* environment variables).
-->

##### OS / ENVIRONMENT
<!---
Mention the OS you are running Ansible from, and the OS you are
managing, or say “N/A” for anything that is not platform-specific.
-->
OEL7

##### SUMMARY
<!--- Explain the problem briefly -->
Using the `meta: reset_connection` instruction appears to be broken under very simple use case.

##### STEPS TO REPRODUCE
<!---
For bugs, show exactly how to reproduce the problem, using a minimal test-case.
For new features, show how the feature would be used.
-->
Using a very simple playbook (below), the `meta: reset_connection` instruction stack traces (command below under `ACUAL RESULTS`)

<!--- Paste example playbooks or commands between quotes below -->
simple `test.yml`:
```yaml
---
- hosts: all
  tasks:
  - meta: reset_connection
```

inventory:
```
# Generated by Vagrant

node01
core ansible_connection=local

[core]
core

[development]
core
```

<!--- You can also paste gist.github.com links for larger files -->

##### EXPECTED RESULTS
<!--- What did you expect to happen when running the steps above? -->
The ssh connection to get reset successfully

##### ACTUAL RESULTS
<!--- What actually happened? If possible run with extra verbosity (-vvvv) -->
Ansible stack traces

<!--- Paste verbatim command output between quotes below -->
```
[vagrant@localhost infra-bootstrap]$ ansible-playbook --limit=core --inventory-file=/tmp/vagrant-ansible/inventory -vvv test.yml
Using /etc/ansible/ansible.cfg as config file
 [WARNING]: Found both group and host with same name: core


PLAYBOOK: test.yml ***************************************************************************************************************************************************
1 plays in test.yml

PLAY [all] ***********************************************************************************************************************************************************

TASK [Gathering Facts] ***********************************************************************************************************************************************
Using module file /usr/lib/python2.7/site-packages/ansible/modules/system/setup.py
<core> ESTABLISH LOCAL CONNECTION FOR USER: vagrant
<core> EXEC /bin/sh -c 'echo ~ && sleep 0'
<core> EXEC /bin/sh -c '( umask 77 && mkdir -p "` echo /home/vagrant/.ansible/tmp/ansible-tmp-1492207079.55-247174731112807 `" && echo ansible-tmp-1492207079.55-247174731112807="` echo /home/vagrant/.ansible/tmp/ansible-tmp-1492207079.55-247174731112807 `" ) && sleep 0'
<core> PUT /tmp/tmpbuhXwS TO /home/vagrant/.ansible/tmp/ansible-tmp-1492207079.55-247174731112807/setup.py
<core> EXEC /bin/sh -c 'chmod u+x /home/vagrant/.ansible/tmp/ansible-tmp-1492207079.55-247174731112807/ /home/vagrant/.ansible/tmp/ansible-tmp-1492207079.55-247174731112807/setup.py && sleep 0'
<core> EXEC /bin/sh -c '/usr/bin/python /home/vagrant/.ansible/tmp/ansible-tmp-1492207079.55-247174731112807/setup.py; rm -rf "/home/vagrant/.ansible/tmp/ansible-tmp-1492207079.55-247174731112807/" > /dev/null 2>&1 && sleep 0'
ok: [core]
META: ran handlers
ERROR! Unexpected Exception: 'NoneType' object has no attribute 'reset'
the full traceback was:

Traceback (most recent call last):
  File "/usr/bin/ansible-playbook", line 109, in <module>
    exit_code = cli.run()
  File "/usr/lib/python2.7/site-packages/ansible/cli/playbook.py", line 154, in run
    results = pbex.run()
  File "/usr/lib/python2.7/site-packages/ansible/executor/playbook_executor.py", line 153, in run
    result = self._tqm.run(play=play)
  File "/usr/lib/python2.7/site-packages/ansible/executor/task_queue_manager.py", line 284, in run
    play_return = strategy.run(iterator, play_context)
  File "/usr/lib/python2.7/site-packages/ansible/plugins/strategy/linear.py", line 221, in run
    results.extend(self._execute_meta(task, play_context, iterator, host))
  File "/usr/lib/python2.7/site-packages/ansible/plugins/strategy/__init__.py", line 908, in _execute_meta
    connection.reset()
AttributeError: 'NoneType' object has no attribute 'reset'
```



Discussion:
@ansibot I can reproduce the issue.
If the robot can accept some command from someone to know if this is an issue can be reproduced...
Looks like this will fail if the config for 'transport = smart'. Using 'ssh' or 'paramiko' works.

Looks like there 'connection_loader.get' fails for 'smart' and ends up returning None and causing the error.
@alikins yes you are right.

Resolving patch:
diff --git a/lib/ansible/executor/task_executor.py b/lib/ansible/executor/task_executor.py
--- a/lib/ansible/executor/task_executor.py
+++ b/lib/ansible/executor/task_executor.py
@@ -20,7 +20,6 @@
 __metaclass__ = type
 
 import base64
-import sys
 import time
 import traceback
 
@@ -722,27 +721,12 @@ def _get_connection(self, variables, templar):
                     if isinstance(i, string_types) and i.startswith("ansible_") and i.endswith("_interpreter"):
                         variables[i] = delegated_vars[i]
 
-        conn_type = self._play_context.connection
-        if conn_type == 'smart':
-            conn_type = 'ssh'
-            if sys.platform.startswith('darwin') and self._play_context.password:
-                # due to a current bug in sshpass on OSX, which can trigger
-                # a kernel panic even for non-privileged users, we revert to
-                # paramiko on that OS when a SSH password is specified
-                conn_type = "paramiko"
-            else:
-                # see if SSH can support ControlPersist if not use paramiko
-                if not check_for_controlpersist(self._play_context.ssh_executable):
-                    conn_type = "paramiko"
-
-        # if someone did `connection: persistent`, default it to using a persistent paramiko connection to avoid problems
-        if conn_type == 'persistent':
-            self._play_context.connection = 'paramiko'
-
-        # if using persistent connections (or the action has set the FORCE_PERSISTENT_CONNECTION attribute to True),
+        # if using persistent paramiko connections (or the action has set the FORCE_PERSISTENT_CONNECTION attribute to True),
         # then we use the persistent connection plugion. Otherwise load the requested connection plugin
-        elif C.USE_PERSISTENT_CONNECTIONS or getattr(self, 'FORCE_PERSISTENT_CONNECTION', False):
+        if C.USE_PERSISTENT_CONNECTIONS or getattr(self, 'FORCE_PERSISTENT_CONNECTION', False):
             conn_type == 'persistent'
+        else:
+            conn_type = self._play_context.connection
 
         connection = self._shared_loader_obj.connection_loader.get(conn_type, self._play_context, self._new_stdin)
 
diff --git a/lib/ansible/playbook/play_context.py b/lib/ansible/playbook/play_context.py
--- a/lib/ansible/playbook/play_context.py
+++ b/lib/ansible/playbook/play_context.py
@@ -26,14 +26,16 @@
 import random
 import re
 import string
+import sys
 
 from ansible import constants as C
 from ansible.errors import AnsibleError
-from ansible.module_utils.six import iteritems, string_types
+from ansible.module_utils.six import iteritems
 from ansible.module_utils.six.moves import shlex_quote
 from ansible.module_utils._text import to_bytes
 from ansible.playbook.attribute import FieldAttribute
 from ansible.playbook.base import Base
+from ansible.utils.ssh_functions import check_for_controlpersist
 
 
 boolean = C.mk_boolean
@@ -241,6 +243,7 @@ def __init__(self, play=None, options=None, passwords=None, connection_lockfd=No
         if play:
             self.set_play(play)
 
+
     def set_play(self, play):
         '''
         Configures this connection information instance with data from
@@ -611,3 +614,28 @@ def update_vars(self, variables):
                         variables[var_opt] = var_val
             except AttributeError:
                 continue
+
+    def _get_attr_connection(self):
+        ''' connections are special, this takes care of responding correctly '''
+        conn_type = None
+        if self._attributes['connection'] == 'smart':
+            conn_type = 'ssh'
+            if sys.platform.startswith('darwin') and self.password:
+                # due to a current bug in sshpass on OSX, which can trigger
+                # a kernel panic even for non-privileged users, we revert to
+                # paramiko on that OS when a SSH password is specified
+                conn_type = "paramiko"
+            else:
+                # see if SSH can support ControlPersist if not use paramiko
+                if not check_for_controlpersist(self.ssh_executable):
+                    conn_type = "paramiko"
+
+        # if someone did `connection: persistent`, default it to using a persistent paramiko connection to avoid problems
+        elif self._attributes['connection'] == 'persistent':
+            conn_type = 'paramiko'
+
+        if conn_type:
+            self.connection = conn_type
+
+        return self._attributes['connection']
+
diff --git a/lib/ansible/plugins/strategy/__init__.py b/lib/ansible/plugins/strategy/__init__.py
--- a/lib/ansible/plugins/strategy/__init__.py
+++ b/lib/ansible/plugins/strategy/__init__.py
@@ -19,6 +19,7 @@
 from __future__ import (absolute_import, division, print_function)
 __metaclass__ = type
 
+import os
 import threading
 import time
 
@@ -903,9 +904,12 @@ def _evaluate_conditional(h):
                         iterator._host_states[host.name].run_state = iterator.ITERATING_COMPLETE
                 msg="ending play"
         elif meta_action == 'reset_connection':
-            connection = connection_loader.get(play_context.connection, play_context, '/dev/null')
-            connection.reset()
-            msg= 'reset connection'
+            connection = connection_loader.get(play_context.connection, play_context, os.devnull)
+            if connection:
+                connection.reset()
+                msg= 'reset connection'
+            else:
+                msg= 'no connection, nothing to reset'
         else:
             raise AnsibleError("invalid meta action requested: %s" % meta_action, obj=task._ds)
 


Test patch:
diff --git a/test/units/playbook/test_play_context.py b/test/units/playbook/test_play_context.py
--- a/test/units/playbook/test_play_context.py
+++ b/test/units/playbook/test_play_context.py
@@ -53,7 +53,7 @@ def tearDown(self):
     def test_play_context(self):
         (options, args) = self._parser.parse_args(['-vv', '--check'])
         play_context = PlayContext(options=options)
-        self.assertEqual(play_context.connection, C.DEFAULT_TRANSPORT)
+        self.assertEqual(play_context._attributes['connection'], C.DEFAULT_TRANSPORT)
         self.assertEqual(play_context.remote_addr, None)
         self.assertEqual(play_context.remote_user, None)
         self.assertEqual(play_context.password, '')