Repository: ansible/ansible-modules-core

Issue:
change pip's 'name' to be a list, to minimize conflicts
##### ISSUE TYPE
- Bug Report
##### ANSIBLE VERSION

```
ansible 2.1.0.0
  config file = /etc/ansible/ansible.cfg
  configured module search path = Default w/o overrides
```
##### CONFIGURATION

export ANSIBLE_LOG_PATH=/var/log/ansible.log
export ANSIBLE_CACHE_PLUGIN=jsonfile
export ANSIBLE_CACHE_PLUGIN_CONNECTION=/var/cache/ansible
export ANSIBLE_PRIVATE_KEY_FILE="/opt/src/_pk/key.pub"

export ANSIBLE_HOST_KEY_CHECKING=false
export ANSIBLE_GATHERING=smart
export ANSIBLE_ROLES_PATH=/opt/src/ansible/roles"
export ANSIBLE_PRIVATE_ROLE_VARS=true
export ANSIBLE_JINJA2_EXTENSIONS=jinja2.ext.do,jinja2.ext.i18n
export ANSIBLE_NOCOWS=1
export ANSIBLE_SSH_ARGS="-o ControlMaster=auto -o ControlPersist=15s -o GSSAPIAuthentication=no -v -o ForwardAgent=yes"

export ANSIBLE_SQUASH_ACTIONS="apk,apt,dnf,package,pacman,pkgng,yum,zypper,pip"
##### OS / ENVIRONMENT

N/A
##### SUMMARY

While trying to install docker-compose and docker-py via the pip module, I noticed a conflict between one of the module versions they required.

If I install via command/pip the problem goes away

To try to keep it "pure ansible" I added "pip" to the list of "squash_actions".
Unfortunately it fails with an error message (see below)
##### STEPS TO REPRODUCE

Original task (not well behaved due to docker-py and docker-compose requiring different versions of "requests")

```
- name: "Docker: install docker python interface"
  become: true
  pip:
    name: "{{ item }}"
    state: latest
  with_items:
    - docker-py
    - docker-compose
```

Workaround via command (not pure ansible)

```
- name: "Docker: install docker python interface"
  become: true
  command: pip install --upgrade docker-py docker-compose
```

add pip to squash_actions

```
export ANSIBLE_SQUASH_ACTIONS="apk,apt,dnf,package,pacman,pkgng,yum,zypper,pip"
```

pip now fails

```
- name: "Docker: install docker python interface"
  become: true
  pip:
    name: "{{ item }}"
    state: latest
  with_items:
    - docker-py
    - docker-compose
```
##### EXPECTED RESULTS

I would expect "pip" to run and install both packages
##### ACTUAL RESULTS

the pip module crashes

```
TASK [ez123-vmhost-docker : Docker: install docker python interface] ***********
task path: /opt/src/ansible/roles/ez123-vmhost-docker/tasks/main.yml:49
<host.dom> ESTABLISH SSH CONNECTION FOR USER: ubuntu
<host.dom> SSH: EXEC ssh -C -q -o ControlMaster=auto -o ControlPersist=15s -o GSSAPIAuthentication=no -v -o ForwardAgent=yes -o StrictHostKeyChecking=no -o Port=60022 -o 'IdentityFile="/opt/src/_pk/id_rsa"' -o KbdInteractiveAuthentication=no -o PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey -o PasswordAuthentication=no -o User=ubuntu -o ConnectTimeout=10 -o ControlPath=/root/.ansible/cp/ansible-ssh-%h-%p-%r host.dom '/bin/sh -c '"'"'( umask 77 && mkdir -p "` echo $HOME/.ansible/tmp/ansible-tmp-1466966432.76-166671552359764 `" && echo ansible-tmp-1466966432.76-166671552359764="` echo $HOME/.ansible/tmp/ansible-tmp-1466966432.76-166671552359764 `" ) && sleep 0'"'"''
<host.dom> PUT /tmp/tmp6snVtE TO /home/ubuntu/.ansible/tmp/ansible-tmp-1466966432.76-166671552359764/pip
<host.dom> SSH: EXEC sftp -b - -C -o ControlMaster=auto -o ControlPersist=15s -o GSSAPIAuthentication=no -v -o ForwardAgent=yes -o StrictHostKeyChecking=no -o Port=60022 -o 'IdentityFile="/opt/src/_pk/id_rsa"' -o KbdInteractiveAuthentication=no -o PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey -o PasswordAuthentication=no -o User=ubuntu -o ConnectTimeout=10 -o ControlPath=/root/.ansible/cp/ansible-ssh-%h-%p-%r '[host.dom]'
<host.dom> ESTABLISH SSH CONNECTION FOR USER: ubuntu
<host.dom> SSH: EXEC ssh -C -q -o ControlMaster=auto -o ControlPersist=15s -o GSSAPIAuthentication=no -v -o ForwardAgent=yes -o StrictHostKeyChecking=no -o Port=60022 -o 'IdentityFile="/opt/src/_pk/id_rsa"' -o KbdInteractiveAuthentication=no -o PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey -o PasswordAuthentication=no -o User=ubuntu -o ConnectTimeout=10 -o ControlPath=/root/.ansible/cp/ansible-ssh-%h-%p-%r -tt host.dom '/bin/sh -c '"'"'sudo -H -S -n -u root /bin/sh -c '"'"'"'"'"'"'"'"'echo BECOME-SUCCESS-hhoupjtvfdzqtmudrbllodbywwtvzpnb; LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8 /usr/bin/python /home/ubuntu/.ansible/tmp/ansible-tmp-1466966432.76-166671552359764/pip; rm -rf "/home/ubuntu/.ansible/tmp/ansible-tmp-1466966432.76-166671552359764/" > /dev/null 2>&1'"'"'"'"'"'"'"'"' && sleep 0'"'"''
failed: [hammer-00] (item=[u'docker-py', u'docker-compose']) => {"cmd": "/usr/local/bin/pip install -U ['docker-py', 'docker-compose']", "failed": true, "invocation": {"module_args": {"chdir": null, "editable": true, "executable": null, "extra_args": null, "name": "['docker-py', 'docker-compose']", "requirements": null, "state": "latest", "umask": null, "use_mirrors": true, "version": null, "virtualenv": null, "virtualenv_command": "virtualenv", "virtualenv_python": null, "virtualenv_site_packages": false}, "module_name": "pip"}, "item": ["docker-py", "docker-compose"], "msg": "\n:stderr: Invalid requirement: '[docker-py,'\nTraceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/pip/req/req_install.py\", line 78, in __init__\n    req = Requirement(req)\n  File \"/usr/local/lib/python2.7/dist-packages/pip/_vendor/packaging/requirements.py\", line 96, in __init__\n    requirement_string[e.loc:e.loc + 8]))\nInvalidRequirement: Invalid requirement, parse error at \"'[docker-'\"\n\n"}

```



Discussion:

Resolving patch:
diff --git a/packaging/language/pip.py b/packaging/language/pip.py
--- a/packaging/language/pip.py
+++ b/packaging/language/pip.py
@@ -36,6 +36,7 @@
   name:
     description:
       - The name of a Python library to install or the url of the remote package.
+      - As of 2.2 you can supply a list of names.
     required: false
     default: null
   version:
@@ -270,19 +271,19 @@ def main():
     module = AnsibleModule(
         argument_spec=dict(
             state=dict(default='present', choices=state_map.keys()),
-            name=dict(default=None, required=False),
-            version=dict(default=None, required=False, type='str'),
-            requirements=dict(default=None, required=False),
-            virtualenv=dict(default=None, required=False),
-            virtualenv_site_packages=dict(default='no', type='bool'),
-            virtualenv_command=dict(default='virtualenv', required=False),
-            virtualenv_python=dict(default=None, required=False, type='str'),
-            use_mirrors=dict(default='yes', type='bool'),
-            extra_args=dict(default=None, required=False),
-            editable=dict(default='yes', type='bool', required=False),
-            chdir=dict(default=None, required=False, type='path'),
-            executable=dict(default=None, required=False),
-            umask=dict(required=False,default=None),
+            name=dict(type='list'),
+            version=dict(type='str'),
+            requirements=dict(),
+            virtualenv=dict(),
+            virtualenv_site_packages=dict(default=False, type='bool'),
+            virtualenv_command=dict(default='virtualenv'),
+            virtualenv_python=dict(type='str'),
+            use_mirrors=dict(default=True, type='bool'),
+            extra_args=dict(),
+            editable=dict(default=True, type='bool'),
+            chdir=dict(type='path'),
+            executable=dict(),
+            umask=dict(),
         ),
         required_one_of=[['name', 'requirements']],
         mutually_exclusive=[['name', 'requirements'], ['executable', 'virtualenv']],
@@ -366,7 +367,12 @@ def main():
 
         # Automatically apply -e option to extra_args when source is a VCS url. VCS
         # includes those beginning with svn+, git+, hg+ or bzr+
-        has_vcs = bool(name and re.match(r'(svn|git|hg|bzr)\+', name))
+        has_vcs = False
+        for pkg in name:
+            if bool(pkg and re.match(r'(svn|git|hg|bzr)\+', pkg)):
+                has_vcs = True
+                break
+
         if has_vcs and module.params['editable']:
             args_list = []  # used if extra_args is not used at all
             if extra_args:
@@ -378,10 +384,12 @@ def main():
 
         if extra_args:
             cmd += ' %s' % extra_args
-        if name:
-            cmd += ' %s' % _get_full_name(name, version)
-        elif requirements:
-            cmd += ' -r %s' % requirements
+
+        for pkg in name:
+            cmd += ' %s' % _get_full_name(pkg, version)
+        else:
+            if requirements:
+                cmd += ' -r %s' % requirements
 
 
         if module.check_mode:
@@ -400,9 +408,11 @@ def main():
             out += out_pip
             err += err_pip
 
-            is_present = _is_present(name, version, out.split())
-
-            changed = (state == 'present' and not is_present) or (state == 'absent' and is_present)
+            for pkg in name:
+                is_present = _is_present(pkg, version, out.split())
+                if (state == 'present' and not is_present) or (state == 'absent' and is_present):
+                    changed = True
+                    break
             module.exit_json(changed=changed, cmd=freeze_cmd, stdout=out, stderr=err)
 
         if requirements or has_vcs:


Test patch: