Repository: ansible/ansible

Issue:
Compatibility with Openbsd "doas"
Hello, I am currently playing with ansible on OpenBSD 5.8 (to be released later this year). I am having issues with privileges escalation, as OpenBSD 5.8 has replaced "sudo" with "doas", cf: 

http://www.tedunangst.com/flak/post/doas
http://www.openbsd.org/58.html

I have tried setting this in my config file: 

```
sudo_exe=/usr/bin/doas
sudo_flags=
```

But I think ansible still pass more parameters to sudo than just "-u", and therefore I get the following error:

```
doas: unknown option -- k
usage: doas [-ns] [-C config] [-u user] command [args]
```

Am I missing something? If not, It would be very helpful to be able to use "doas" as a replacement for "sudo" in ansible!

Thanks!



Discussion:
I just gave this a quick shot, turns out it won't be as easy to implement as I had hoped. Doas does not allow to set environment variables before running an actual command:

bash-4.3# doas "ls"
bash-4.3# doas "LANG=en_US.UTF-8 ls"
doas: LANG=en_US.UTF-8 ls: command not found

And this is massively used by ansible.

@mutemule this is awesome, thank you very much! I didn't know you could pass environment variables to doas using the env keyword. 

[Edit]: ok, I get the trick now.

@yannh Yeah, it's functional, but it's not ideal. It would be nice if ansible passed along the environment variables and the commands to run as distinct variables, so environment variables could be handled appropriately.

However, given the constraints we have, this does work.


Resolving patch:
diff --git a/lib/ansible/constants.py b/lib/ansible/constants.py
--- a/lib/ansible/constants.py
+++ b/lib/ansible/constants.py
@@ -162,8 +162,8 @@ def shell_expand_path(path):
 DEFAULT_ASK_SUDO_PASS     = get_config(p, DEFAULTS, 'ask_sudo_pass',    'ANSIBLE_ASK_SUDO_PASS',    False, boolean=True)
 
 # Become
-BECOME_ERROR_STRINGS      = {'sudo': 'Sorry, try again.', 'su': 'Authentication failure', 'pbrun': '', 'pfexec': '', 'runas': ''} #FIXME: deal with i18n
-BECOME_METHODS            = ['sudo','su','pbrun','pfexec','runas']
+BECOME_ERROR_STRINGS      = {'sudo': 'Sorry, try again.', 'su': 'Authentication failure', 'pbrun': '', 'pfexec': '', 'runas': '', 'doas': 'Permission denied'} #FIXME: deal with i18n
+BECOME_METHODS            = ['sudo','su','pbrun','pfexec','runas','doas']
 DEFAULT_BECOME_METHOD     = get_config(p, 'privilege_escalation', 'become_method', 'ANSIBLE_BECOME_METHOD','sudo' if DEFAULT_SUDO else 'su' if DEFAULT_SU else 'sudo' ).lower()
 DEFAULT_BECOME            = get_config(p, 'privilege_escalation', 'become', 'ANSIBLE_BECOME',False, boolean=True)
 DEFAULT_BECOME_USER       = get_config(p, 'privilege_escalation', 'become_user', 'ANSIBLE_BECOME_USER', 'root')
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
@@ -387,6 +387,20 @@ def detect_su_prompt(data):
                 flags = self.become_flags or ''
                 becomecmd = '%s %s /user:%s "%s"' % (exe, flags, self.become_user, success_cmd)
 
+            elif self.become_method == 'doas':
+
+                prompt = 'Password:'
+                exe = self.become_exe or 'doas'
+                flags = self.become_flags or ''
+
+                if not self.become_pass:
+                    flags += ' -n '
+
+                if self.become_user:
+                    flags += ' -u %s ' % self.become_user
+
+                becomecmd = '%s %s echo %s && %s %s env ANSIBLE=true %s' % (exe, flags, success_key, exe, flags, cmd)
+
             else:
                 raise AnsibleError("Privilege escalation method not found: %s" % self.become_method)
 


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
@@ -123,6 +123,8 @@ def test_play_context_make_become_cmd(self):
         pbrun_flags = ''
         pfexec_exe   = 'pfexec'
         pfexec_flags = ''
+        doas_exe    = 'doas'
+        doas_flags  = ' -n  -u foo '
 
         cmd = play_context.make_become_cmd(cmd=default_cmd, executable=default_exe)
         self.assertEqual(cmd, default_cmd)
@@ -146,6 +148,10 @@ def test_play_context_make_become_cmd(self):
         cmd = play_context.make_become_cmd(cmd=default_cmd, executable="/bin/bash")
         self.assertEqual(cmd, """%s -c '%s %s "'"'"'echo %s; %s'"'"'"'""" % (default_exe, pfexec_exe, pfexec_flags, play_context.success_key, default_cmd))
 
+        play_context.become_method = 'doas'
+        cmd = play_context.make_become_cmd(cmd=default_cmd, executable="/bin/bash")
+        self.assertEqual(cmd, """%s -c '%s %s echo %s && %s %s env ANSIBLE=true %s'""" % (default_exe, doas_exe, doas_flags, play_context.success_key, doas_exe, doas_flags, default_cmd))
+
         play_context.become_method = 'bad'
         self.assertRaises(AnsibleError, play_context.make_become_cmd, cmd=default_cmd, executable="/bin/bash")