Repository: ansible/ansible

Issue:
Sudo/Become doesnt work on local connection in loops
I am having a confusing problem. It does look like a bug and hence I am reporting here, not sure if duplicate.

I am using ansible version 2.0.0 locally on OSX El Capitan, to run a series of steps which sets up my machine for development for my project. The last step here is to add a few lines to `/etc/hosts` (~15 lines)
I am using `lineinfile` to add these lines in a loop. 

I have tried `become: root` with `become_ask_pass=True` in the cfg file. This does nothing. It does not ask my password and once it hits that task, I get `The destination directory (/private/etc) is not writable by the current user.`. If I try `sudo: yes` and `ask_sudo_pass=True` then I get `fatal: [localhost]: FAILED! => {"failed": true, "msg": "ERROR! privilege output closed while waiting for password prompt:\nBECOME-SUCCESS-jpibjiakmfjdrjafspqizphqotvhjikm\n{\"msg\": \"line added\", \"changed\": true, \"backup\": \"\"}\n"}`. But infact If i check now, `/etc/hosts` file now has the first 2 lines of the list of lines I had to add. Running the playbook now doesnt help me at all.. but even more oddly, If i remove the config option to ask for password and run the play it works fully.... 



Discussion:
This is the play to add the lines to the file. It is one of 4 plays in this playbook.

```
- name: /etc/hosts file setup
  hosts: all
  tasks:
    - name: Add the /etc/hosts lines
      lineinfile: dest=/etc/hosts line='{{item}}'
      sudo: yes
      with_items:
        - 127.0.0.1       www.dev.example.com
        - 127.0.0.1       th.dev.example.com vn.dev.example.com id.dev.example.com de.dev.example.com
        - 127.0.0.1       admin.dev.example.com
        - 127.0.0.1       api.dev.example.com
        - 127.0.0.1       static.dev.example.com static0.dev.example.com static1.dev.example.com static2.dev.example.com static3.dev.example.com
        - 127.0.0.1       db.dev.example.com
        - 127.0.0.1       search.dev.example.com
        - 127.0.0.1       cache.dev.example.com
        - 127.0.0.1       queue.dev.example.com
        - "   "
        - 127.0.0.1       www.dev.exampleb.asia
        - 127.0.0.1       www.dev.exampleb.com.sg
        - 127.0.0.1       admin.dev.exampleb.asia
        - 127.0.0.1       api.dev.exampleb.asia
        - 127.0.0.1       static.dev.exampleb.asia static0.dev.exampleb.asia static1.dev.exampleb.asia static2.dev.exampleb.asia static3.dev.exampleb.asia
        - 127.0.0.1       static.dev.exampleb.com.sg static0.dev.exampleb.com.sg static1.dev.exampleb.com.sg static2.dev.exampleb.com.sg static3.dev.exampleb.com.sg
        - 127.0.0.1       db.dev.exampleb.asia
        - 127.0.0.1       search.dev.exampleb.asia
        - 127.0.0.1       cache.dev.exampleb.asia
        - 127.0.0.1       queue.dev.exampleb.asia
        - " "
        - 54.192.146.88   ugc.dev.exampleb.asia
        - 54.192.146.88   ugc.dev.exampleb.com.sg
```

This appears a problem with become/lineinfile combination. I switched to blockinfile for now, and it seems to work fine.

The Trouble is coming from https://github.com/ansible/ansible/blob/devel/lib/ansible/plugins/connection/local.py#L110 and it appears to have been last edited in a commit titled "partial become support for local connection plugin " so seems like perhaps half done??

Minimal test case - 

```
---
- name: Failing test case for become/sudo with lineinfile
  hosts: all
  tasks:
    - name: Test
      lineinfile: dest=text.file line={{item}}
      sudo: yes
      with_items:
        - "1"
        - "2"
        - "3"
        - "11"
        - "12"
        - "13"
```

This bug is to do with `sudo` and `local` connection. Replacing `lineinfile` with `command: echo {{item}}` results in the same error, again after 2 successful invocations

I am going to guess that perhaps this is because on attempt one, `sudo` password is asked, and by attempt 2 `sudo` doesnt need a password. Thus the command completes but looks like an error and ansible stops processing the rest

I think I am onto something.. If i force `sudo` to ask for password everytime, as in http://askubuntu.com/a/636095/92812, then I trigger the timeout error. "ERROR! timeout waiting for privilege escalation password prompt"

See #13728 

Hi @amenonsen , I think I have seen that error but I believe it may be unrelated. What I am seeing right now in my testing is `self._play_context.success_key` is not updated with the new key after the first task, thus the second task is not detected as a success and the rest of the tasks are ignored.

with -vvv and print command printing the `success_key` and the output at `check_become_success` I see 

```
ESTABLISH LOCAL CONNECTION FOR USER: karthikt
localhost EXEC ( umask 22 && mkdir -p "$( echo $HOME/.ansible/tmp/ansible-tmp-1452496967.86-266592598499673 )" && echo "$( echo $HOME/.ansible/tmp/ansible-tmp-1452496967.86-266592598499673 )" )
localhost PUT /var/folders/0s/62grd4717gd2v242bzjtw9l00000gn/T/tmpGiRWin TO /Users/karthikt/.ansible/tmp/ansible-tmp-1452496967.86-266592598499673/lineinfile
localhost EXEC /bin/sh -c 'sudo -H -S  -p "[sudo via ansible, key=winpbukpcrihtehcdvgurdxckejmbmiu] password: " -u root /bin/sh -c '"'"'echo BECOME-SUCCESS-winpbukpcrihtehcdvgurdxckejmbmiu; LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8 /usr/bin/python /Use
rs/karthikt/.ansible/tmp/ansible-tmp-1452496967.86-266592598499673/lineinfile; rm -rf "/Users/karthikt/.ansible/tmp/ansible-tmp-1452496967.86-266592598499673/" > /dev/null 2>&1'"'"''
[sudo via ansible, key=winpbukpcrihtehcdvgurdxckejmbmiu] password:
BECOME-SUCCESS-winpbukpcrihtehcdvgurdxckejmbmiu
BECOME-SUCCESS-winpbukpcrihtehcdvgurdxckejmbmiu BECOME-SUCCESS-winpbukpcrihtehcdvgurdxckejmbmiu
BECOME-SUCCESS-winpbukpcrihtehcdvgurdxckejmbmiu BECOME-SUCCESS-winpbukpcrihtehcdvgurdxckejmbmiu
localhost EXEC ( umask 22 && mkdir -p "$( echo $HOME/.ansible/tmp/ansible-tmp-1452496967.94-179979904314244 )" && echo "$( echo $HOME/.ansible/tmp/ansible-tmp-1452496967.94-179979904314244 )" )
localhost PUT /var/folders/0s/62grd4717gd2v242bzjtw9l00000gn/T/tmpJZemFm TO /Users/karthikt/.ansible/tmp/ansible-tmp-1452496967.94-179979904314244/lineinfile
localhost EXEC /bin/sh -c 'sudo -H -S  -p "[sudo via ansible, key=yesxrbpmewejqiuizhjjgamvmppjrvyy] password: " -u root /bin/sh -c '"'"'echo BECOME-SUCCESS-yesxrbpmewejqiuizhjjgamvmppjrvyy; LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8 /usr/bin/python /Use
rs/karthikt/.ansible/tmp/ansible-tmp-1452496967.94-179979904314244/lineinfile; rm -rf "/Users/karthikt/.ansible/tmp/ansible-tmp-1452496967.94-179979904314244/" > /dev/null 2>&1'"'"''
[sudo via ansible, key=winpbukpcrihtehcdvgurdxckejmbmiu] password:
BECOME-SUCCESS-winpbukpcrihtehcdvgurdxckejmbmiu
BECOME-SUCCESS-winpbukpcrihtehcdvgurdxckejmbmiu BECOME-SUCCESS-yesxrbpmewejqiuizhjjgamvmppjrvyy
BECOME-SUCCESS-winpbukpcrihtehcdvgurdxckejmbmiu BECOME-SUCCESS-yesxrbpmewejqiuizhjjgamvmppjrvyy
{"msg": "", "changed": false, "backup": ""}
```

The line `BECOME-SUCCESS-winpbukpcrihtehcdvgurdxckejmbmiu BECOME-SUCCESS-yesxrbpmewejqiuizhjjgamvmppjrvyy` is the crux of my problem.

Actually something is wrong `_play_context` here. I think an old one is being preserved and that is the bug. 

in `actions/__init__.py` -> `_low_level_execute_command` if I print `self._play_context.success_key`, it is correct. 

However if I print the same in `local.py` in the `if self._play_context.prompt and sudoable:` block. I see old values.  This is both to `success_key` and `prompt` which leads me to believe the wrong `_play_context` is being accessed.

I add `display.vvv(self._play_context == self._connection._play_context)` just before `rc, stdout, stderr = self._connection.exec_command(cmd, in_data=in_data, sudoable=sudoable)` in `_low_level_execute_command`. In the first iteration it is true. In the second it is **false**.

> I think I have seen that error but I believe it may be unrelated

Scroll down further and read @Yannig's comments.

Yup @amenonsen  his one line fix exactly reflects my understanding so far. 

I am looking at `self._play_context = self._play_context.set_task_and_variable_override(task=self._task, variables=variables, templar=templar)
` 
which might be the line which is causing the `_play_context` to change. and @Yannig 's fix will keep that change in sync with `_connection`

I am curious though how come this issue and the workaround is happening in base classes. Since the error seems restricted to the local connection module (or is it broken on other connection types as well??

It's probably broken on other connection types as well, but the breakage is hidden. @mgedmin had definitely filed some report showing the the wrong success_key was being looked for in the ssh plugin (but I don't remember the issue number).

@amenonsen I feel they are still 2 seperate problems. His issue is multiline outputs arent handled well in local.py. My issue is looping breaking `_play_context`

Confirming the error with `become: True` and a local connection. The other factor I see is that `-K` parameter has been used to request a password - without the `-K` parameter, but after `sudo` has been "primed" by the user giving the password, Ansible works fine. Here's an example playbook I used for testing:

```
---
- hosts: all
  become: True

  tasks:
    - name: Make sure required directories exist
      file:
        path: '{{ item }}'
        state: 'directory'
      with_items: [ '/bin', '/etc' ]
```

I have bisected the issue to this commit: https://github.com/ansible/ansible/commit/272778f732031eb98d417c4dd4f6947cdef08aea

Thanks @drybjed, Just the commit title directly indicates the problem "Modify task executor to **reuse connection inside a loop**"

@amenonsen are we not checking if they match? If we check then it cant remain hidden right? 

@drybjed I am able to confirm that the "fix" is to disable that one line I have commented on above.

As an aside.. Did I just discover a github bug?? I added a comment but github says I pushed a commit.

Should I make a PR removing that? Or is there a reason for it?

I was able to fix the issue by adding 

```
    self._connection._play_context = self._play_context
```

Below that `if` block. I think I will add that as a PR.


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
@@ -365,6 +365,9 @@ def _execute(self, variables=None):
         if not self._connection or not getattr(self._connection, 'connected', False):
             self._connection = self._get_connection(variables=variables, templar=templar)
             self._connection.set_host_overrides(host=self._host)
+        #If connection is reused, its _play_context is no longer valid and needs to be replaced
+        #This fixes issues with tasks running sudo in a loop and having the success_key incorrect in the second iteration
+        self._connection._play_context = self._play_context
 
         self._handler = self._get_action_handler(connection=self._connection, templar=templar)
 


Test patch: