Repository: ansible/ansible

Issue:
ansible-galaxy: Traceback when role archive is extracted in cwd
##### ISSUE TYPE
- Bug Report
##### ANSIBLE VERSION

ansible 2.1.0 (devel 2163a24a12) last updated 2016/04/13 21:09:47 (GMT -400)
##### CONFIGURATION

Not relevant
##### OS / ENVIRONMENT

Not specific to an environment but reproduced under Fedora 23.
##### SUMMARY

Retrieving a role that is in a .tar.gz archive that extracts the role inside the CWD instead of a subfolder causes the role installation to fail.

The use case was to download archives provided by Gerrit repositories since ansible-galaxy is not able to fetch a reference before checking out a revision.
Archives provided by gerrit are compressed from inside the repository while archives provided by github are compressed from outside the repository and into a subfolder.
##### STEPS TO REPRODUCE

```
$ mkdir -p /tmp/test; cd /tmp/test
$ git clone https://github.com/name/ansible-role-repository

$ tar -czf ansible-role-repository-folder.tar.gz ansible-role-repository
$ cd ansible-role-repository
$ tar -czf ansible-role-repository-cwd.tar.gz *; mv ansible-role-repository-cwd.tar.gz ..
# Upload the two archives somewhere

$ cat << EOF > ansible-role-requirements.yml
- name: role-folder
  src: http://somewhere/ansible-role-folder.tar.gz
- name: role-cwd
  src: http://somewhere/ansible-role-cwd.tar.gz
EOF

$ ansible-galaxy install -r ansible-role-requirements.yml -vvv --force
Opened /home/dmsimard/.ansible_galaxy
Validate TLS certificates: True
Connecting to galaxy_server: https://galaxy.ansible.com
Base API: https://galaxy.ansible.com/api/v1
found role {'scm': None, 'src': 'http://somewhere/ansible-role-folder.tar.gz', 'version': '', 'name': 'role-folder'} in yaml file
Validate TLS certificates: True
found role {'scm': None, 'src': 'http://somewhere/ansible-role-cwd.tar.gz', 'version': '', 'name': 'role-cwd'} in yaml file
Validate TLS certificates: True
Installing role role-folder 
- downloading role from http://somewhere/ansible-role-folder.tar.gz
- extracting role-folder to playbooks/roles/role-folder
- role-folder was installed successfully
Installing role role-cwd 
- downloading role from http://somewhere/ansible-role-cwd.tar.gz
- extracting role-cwd to playbooks/roles/role-cwd
ERROR! Unexpected Exception: join() takes at least 1 argument (0 given)
the full traceback was:

Traceback (most recent call last):
  File "/home/dmsimard/dev/openstack/src/ansible/bin/ansible-galaxy", line 92, in <module>
    exit_code = cli.run()
  File "/home/dmsimard/dev/openstack/src/ansible/lib/ansible/cli/galaxy.py", line 157, in run
    self.execute()
  File "/home/dmsimard/dev/openstack/src/ansible/lib/ansible/cli/__init__.py", line 100, in execute
    fn()
  File "/home/dmsimard/dev/openstack/src/ansible/lib/ansible/cli/galaxy.py", line 409, in execute_install
    installed = role.install()
  File "/home/dmsimard/dev/openstack/src/ansible/lib/ansible/galaxy/role.py", line 291, in install
    member.name = os.path.join(*final_parts)
TypeError: join() takes at least 1 argument (0 given)
```
##### EXPECTED RESULTS

The role should install properly even if it is extracted in the current working directory instead of a subfolder.
##### ACTUAL RESULTS

See steps to reproduce.



Discussion:

Resolving patch:
diff --git a/lib/ansible/galaxy/role.py b/lib/ansible/galaxy/role.py
--- a/lib/ansible/galaxy/role.py
+++ b/lib/ansible/galaxy/role.py
@@ -263,8 +263,17 @@ def install(self):
                 # next find the metadata file
                 for member in members:
                     if self.META_MAIN in member.name:
-                        meta_file = member
-                        break
+                        # Look for parent of meta/main.yml
+                        # Due to possibility of sub roles each containing meta/main.yml
+                        # look for shortest length parent
+                        meta_parent_dir = os.path.dirname(os.path.dirname(member.name))
+                        if not meta_file:
+                            archive_parent_dir = meta_parent_dir
+                            meta_file = member
+                        else:
+                            if len(meta_parent_dir) < len(archive_parent_dir):
+                                archive_parent_dir = meta_parent_dir
+                                meta_file = member
                 if not meta_file:
                     raise AnsibleError("this role does not appear to have a meta/main.yml file.")
                 else:
@@ -273,9 +282,9 @@ def install(self):
                     except:
                         raise AnsibleError("this role does not appear to have a valid meta/main.yml file.")
 
-                # we strip off the top-level directory for all of the files contained within
-                # the tar file here, since the default is 'github_repo-target', and change it
-                # to the specified role's name
+                # we strip off any higher-level directories for all of the files contained within
+                # the tar file here. The default is 'github_repo-target'. Gerrit instances, on the other
+                # hand, does not have a parent directory at all.
                 installed = False
                 while not installed:
                     display.display("- extracting %s to %s" % (self.name, self.path))
@@ -296,9 +305,9 @@ def install(self):
                         for member in members:
                             # we only extract files, and remove any relative path
                             # bits that might be in the file for security purposes
-                            # and drop the leading directory, as mentioned above
+                            # and drop any containing directory, as mentioned above
                             if member.isreg() or member.issym():
-                                parts = member.name.split(os.sep)[1:]
+                                parts = member.name.replace(archive_parent_dir, "").split(os.sep)
                                 final_parts = []
                                 for part in parts:
                                     if part != '..' and '~' not in part and '$' not in part:


Test patch: