Repository: ansible/ansible

Issue:
One or more SELinux operations fail when path is given in complex args
Passing paths as complex args in playbooks makes them end up as unicode instances in the module, as opposed to the str instances you get when using key=value arguments.  selinux.matchpathcon(), and probably other functions in the selinux module, raise TypeError when given unicode instances.

For example, this playbook fails on my CentOS 6 system with SELinux in enforcing mode and python-selinux installed:

```
- hosts: all
  tasks:
    - action: copy
      args:
        src: some_file
        dest: /tmp/some_other_file
```

The exception is as follows:

```
Traceback (most recent call last):
  File "/home/user/.ansible/tmp/ansible-1365569148.43-637871259277/copy", line 998, in <module>
    main()
  File "/home/user/.ansible/tmp/ansible-1365569148.43-637871259277/copy", line 142, in main
    module.atomic_replace(dest_tmp, dest)
  File "/home/user/.ansible/tmp/ansible-1365569148.43-637871259277/copy", line 927, in atomic_replace
    context = self.selinux_default_context(dest)
  File "/home/user/.ansible/tmp/ansible-1365569148.43-637871259277/copy", line 409, in selinux_default_context
    ret = selinux.matchpathcon(path, mode)
TypeError: in method 'matchpathcon', argument 1 of type 'char const *'
```

In this example the `path` being passed to matchpathcon is the `dest` argument to the task.  It is unicode and not a str because the complex arguments are passed as JSON, and decoding with Python's json module results in unicode instances.

I don't exactly know if/how this should be fixed.  Is the real problem in python-selinux?  Should Ansible make sure to first convert all potential arguments to selinux functions to strs?  (`dest = str(module.params["dest"])`, and non-ASCII paths be damned?)



Discussion:
I would assume it would accept UTF-8 encoded strings and we should just decode them if we get a unicode here.

Thanks for the heads up!


Resolving patch:
diff --git a/lib/ansible/module_common.py b/lib/ansible/module_common.py
--- a/lib/ansible/module_common.py
+++ b/lib/ansible/module_common.py
@@ -271,13 +271,26 @@ def selinux_initial_context(self):
             context.append(None)
         return context
 
+    def _to_filesystem_str(self, path):
+        '''Returns filesystem path as a str, if it wasn't already.
+
+        Used in selinux interactions because it cannot accept unicode
+        instances, and specifying complex args in a playbook leaves
+        you with unicode instances.  This method currently assumes
+        that your filesystem encoding is UTF-8.
+
+        '''
+        if isinstance(path, unicode):
+            path = path.encode("utf-8")
+        return path
+
     # If selinux fails to find a default, return an array of None
     def selinux_default_context(self, path, mode=0):
         context = self.selinux_initial_context()
         if not HAVE_SELINUX or not self.selinux_enabled():
             return context
         try:
-            ret = selinux.matchpathcon(path, mode)
+            ret = selinux.matchpathcon(self._to_filesystem_str(path), mode)
         except OSError:
             return context
         if ret[0] == -1:
@@ -290,7 +303,7 @@ def selinux_context(self, path):
         if not HAVE_SELINUX or not self.selinux_enabled():
             return context
         try:
-            ret = selinux.lgetfilecon(path)
+            ret = selinux.lgetfilecon(self._to_filesystem_str(path))
         except OSError, e:
             if e.errno == errno.ENOENT:
                 self.fail_json(path=path, msg='path %s does not exist' % path)
@@ -340,7 +353,8 @@ def set_context_if_different(self, path, context, changed):
             try:
                 if self.check_mode:
                     return True
-                rc = selinux.lsetfilecon(path, ':'.join(new_context))
+                rc = selinux.lsetfilecon(self._to_filesystem_str(path),
+                                         str(':'.join(new_context)))
             except OSError:
                 self.fail_json(path=path, msg='invalid selinux context', new_context=new_context, cur_context=cur_context, input_was=context)
             if rc != 0:


Test patch: