Repository: ansible/ansible

Issue:
ansible/parsing/dataloader.py tries to use formatting with bytes string (uncompatible with py3)
<!---
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
 - Bug Report


##### COMPONENT NAME
when using "copy", programming error in ansible/parsing/dataloader.py

##### ANSIBLE VERSION
<!--- Paste verbatim output from “ansible --version” between quotes below -->
```
ansible 2.3.0.0
  config file = /home/centos/os-data-miner-jupyter-ansible/ansible.cfg
  configured module search path = Default w/o overrides
  python version = 3.4.5 (default, Nov  9 2016, 16:24:59) [GCC 4.8.5 20150623 (Red Hat 4.8.5-11)]
```

##### CONFIGURATION


##### OS / ENVIRONMENT
N/A

##### SUMMARY
when using "copy", there's a programming error in ansible/parsing/dataloader.py

##### STEPS TO REPRODUCE
$ python3
>>> for suffix in (b'.yml', b'.yaml', b''):
...     b_main = b'main%s' % (suffix)
...
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
TypeError: unsupported operand type(s) for %: 'bytes' and 'bytes'

##### EXPECTED RESULTS
Code running fine

##### ACTUAL RESULTS
<!--- What actually happened? If possible run with extra verbosity (-vvvv) -->
Exception
<!--- Paste verbatim command output between quotes below -->
```
task path: /home/centos/os-data-miner-jupyter-ansible/jupyterhub-install.yaml:31
The full traceback is:
Traceback (most recent call last):
  File "/usr/lib/python3.4/site-packages/ansible/executor/task_executor.py", line 125, in run
    res = self._execute()
  File "/usr/lib/python3.4/site-packages/ansible/executor/task_executor.py", line 521, in _execute
    result = self._handler.run(task_vars=variables)
  File "/usr/lib/python3.4/site-packages/ansible/plugins/action/copy.py", line 97, in run
    source = self._find_needle('files', source)
  File "/usr/lib/python3.4/site-packages/ansible/plugins/action/__init__.py", line 986, in _find_needle
    result = self._loader.path_dwim_relative_stack(path_stack, dirname, needle)
  File "/usr/lib/python3.4/site-packages/ansible/parsing/dataloader.py", line 330, in path_dwim_relative_stack
    if is_role or self._is_role(path):
  File "/usr/lib/python3.4/site-packages/ansible/parsing/dataloader.py", line 237, in _is_role
    b_main = b'main%s' % (suffix)
TypeError: unsupported operand type(s) for %: 'bytes' and 'bytes'

fatal: [up-premium]: FAILED! => {
    "failed": true,
    "msg": "Unexpected failure during module execution.",
    "stdout": ""
}
```



Discussion:

Resolving patch:
diff --git a/lib/ansible/parsing/dataloader.py b/lib/ansible/parsing/dataloader.py
--- a/lib/ansible/parsing/dataloader.py
+++ b/lib/ansible/parsing/dataloader.py
@@ -1,39 +1,27 @@
 # (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
-#
-# This file is part of Ansible
-#
-# Ansible is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# Ansible is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
+# Copyright: (c) 2017, Ansible Project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
 
 # Make coding more python3-ish
 from __future__ import (absolute_import, division, print_function)
 __metaclass__ = type
 
 import copy
-import os
 import json
+import os
+import os.path
 import re
 import tempfile
+
 from yaml import YAMLError
 
-from ansible.module_utils.six import text_type, string_types
 from ansible.errors import AnsibleFileNotFound, AnsibleParserError
 from ansible.errors.yaml_strings import YAML_SYNTAX_ERROR
 from ansible.module_utils.basic import is_executable
-from ansible.module_utils.six import binary_type, text_type
+from ansible.module_utils.six import binary_type, string_types, text_type
 from ansible.module_utils._text import to_bytes, to_native, to_text
-from ansible.parsing.vault import VaultLib, b_HEADER, is_encrypted, is_encrypted_file, parse_vaulttext_envelope
 from ansible.parsing.quoting import unquote
+from ansible.parsing.vault import VaultLib, b_HEADER, is_encrypted, is_encrypted_file, parse_vaulttext_envelope
 from ansible.parsing.yaml.loader import AnsibleLoader
 from ansible.parsing.yaml.objects import AnsibleBaseYAMLObject, AnsibleUnicode
 from ansible.utils.path import unfrackpath
@@ -44,6 +32,7 @@
     from ansible.utils.display import Display
     display = Display()
 
+
 # Tries to determine if a path is inside a role, last dir must be 'tasks'
 # this is not perfect but people should really avoid 'tasks' dirs outside roles when using Ansible.
 RE_TASKS = re.compile(u'(?:^|%s)+tasks%s?$' % (os.path.sep, os.path.sep))
@@ -270,10 +259,10 @@ def _is_role(self, path):
         b_path = to_bytes(path, errors='surrogate_or_strict')
         b_upath = to_bytes(unfrackpath(path, follow=False), errors='surrogate_or_strict')
 
-        for finddir in (b'meta', b'tasks'):
-            for suffix in (b'.yml', b'.yaml', b''):
-                b_main = b'main%s' % (suffix)
-                b_tasked = b'%s/%s' % (finddir, b_main)
+        for b_finddir in (b'meta', b'tasks'):
+            for b_suffix in (b'.yml', b'.yaml', b''):
+                b_main = b'main%s' % (b_suffix)
+                b_tasked = os.path.join(b_finddir, b_main)
 
                 if (
                     RE_TASKS.search(path) and


Test patch:
diff --git a/test/units/parsing/test_dataloader.py b/test/units/parsing/test_dataloader.py
--- a/test/units/parsing/test_dataloader.py
+++ b/test/units/parsing/test_dataloader.py
@@ -41,6 +41,12 @@ def setUp(self):
     def tearDown(self):
         pass
 
+    @patch('os.path.exists')
+    def test__is_role(self, p_exists):
+        p_exists.side_effect = lambda p: p == b'test_path/tasks/main.yml'
+        self.assertTrue(self._loader._is_role('test_path/tasks'))
+        self.assertTrue(self._loader._is_role('test_path/'))
+
     @patch.object(DataLoader, '_get_file_contents')
     def test_parse_json_from_file(self, mock_def):
         mock_def.return_value = (b"""{"a": 1, "b": 2, "c": 3}""", True)