Repository: ansible/ansible-modules-core

Issue:
apt_repository with ppa fails on Linux Mint 18 (Sarah)
<!--- Verify first that your issue/request is not already reported in GitHub -->
##### ISSUE TYPE
- Bug Report
##### COMPONENT NAME

apt_repository
##### ANSIBLE VERSION

<!--- Paste verbatim output from “ansible --version” between quotes below -->

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

<!---
Mention any settings you have changed/added/removed in ansible.cfg
(or using the ANSIBLE_* environment variables).
-->

```
[defaults]
ansible_managed = Ansible managed: {file} on {host}
fact_caching = memory
forks = 50
host_key_checking = false
pattern = *
retry_files_enabled = false
var_compression_level = 6

[ssh_connection]
pipelining = True
ssh_args = -o ControlMaster=auto -o ControlPersist=60s -o ForwardAgent=yes
```
##### OS / ENVIRONMENT

<!---
Mention the OS you are running Ansible from, and the OS you are
managing, or say “N/A” for anything that is not platform-specific.
-->

```
Linux frodo 4.7.0 #1 SMP Tue Jul 26 08:50:06 BST 2016 x86_64 x86_64 x86_64 GNU/Linux
Linux Mint 18 Sarah 
```
##### SUMMARY

<!--- Explain the problem briefly -->

apt_repository fails on Linux Mint 18 when using repo with ppa:foo

This is mentioned here: https://github.com/ansible/ansible/issues/4587 however that ticket has been closed.
##### STEPS TO REPRODUCE

<!---
For bugs, show exactly how to reproduce the problem.
For new features, show how the feature would be used.
-->

Create a simple main.yml containing this:

```
- name: default Linux configuration
  become: yes
  hosts: linux
  roles:
    - {role: apt, tags: ['apt']}
```

Create a simple roles/apt/tasks/main.yml, containing, for example this:

```
- name: add git ppa
  apt_repository: repo="{{ item }}" update_cache="yes"
  with_items:
    - ppa:git-core/ppa
```

Then run:

<!--- Paste example playbooks or commands between quotes below -->

```
ansible-playbook -i inventory/linux/hosts.ini -t apt main.yml
```

<!--- You can also paste gist.github.com links for larger files -->
##### EXPECTED RESULTS

<!--- What did you expect to happen when running the steps above? -->

Something similar to this:

```
ok: [foo] => (item=ppa:git-core/ppa)
```
##### ACTUAL RESULTS

<!--- What actually happened? If possible run with extra verbosity (-vvvv) -->

<!--- Paste verbatim command output between quotes below -->

```
failed: [foo] (item=ppa:git-core/ppa) => {"failed": true, "invocation": {"module_args": {"filename": null, "install_python_apt": true, "mode": "420", "repo": "ppa:git-core/ppa", "state": "present", "update_cache": true, "validate_certs": true}, "module_name": "apt_repository"}, "item": "ppa:git-core/ppa", "msg": "Invalid repository string: ppa:git-core/ppa"}
```

This does not happen on vanilla Debian or Ubuntu machines.



Discussion:
<!-- @dharrigan: Thanks for filing this bug report! -->

@ansible ping, this issue is waiting for your response.
[click here for bot help](https://github.com/jctanner/ansibullbot/blob/ISSUE_TRIAGE_CLASS/ISSUE_HELP.md)

<!--- boilerplate: issue_bug_new --->

@ansible, ping. This issue is still waiting on your response.
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)

<!--- boilerplate: issue_renotify_maintainer --->

@ansible, ping. This issue is still waiting on your response.
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)

<!--- boilerplate: issue_renotify_maintainer --->

@ansible, ping. This issue is still waiting on your response.
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)

<!--- boilerplate: issue_renotify_maintainer --->

Hi, any chance of this getting any attention? There are several temporary solutions out there, having one of them in the core should be better.


Resolving patch:
diff --git a/packaging/os/apt_repository.py b/packaging/os/apt_repository.py
--- a/packaging/os/apt_repository.py
+++ b/packaging/os/apt_repository.py
@@ -28,9 +28,8 @@
 description:
     - Add or remove an APT repositories in Ubuntu and Debian.
 notes:
-    - This module works on Debian and Ubuntu.
+    - This module works on Debian, Ubuntu and their derivatives.
     - This module supports Debian Squeeze (version 6) as well as its successors.
-    - This module treats Debian and Ubuntu distributions separately. So PPA could be installed only on Ubuntu machines.
 options:
     repo:
         required: true
@@ -70,6 +69,12 @@
               Defaults to a file name based on the repository source url.
               The .list extension will be automatically added.
         required: false
+    codename:
+        version_added: '2.3'
+        description:
+            - Override the distribution codename to use for PPA repositories.
+              Should usually only be set when working with a PPA on a non-Ubuntu target (e.g. Debian or Mint)
+        required: false
 author: "Alexander Saltanov (@sashka)"
 version_added: "0.7"
 requirements:
@@ -90,9 +95,11 @@
 # Remove specified repository from sources list.
 apt_repository: repo='deb http://archive.canonical.com/ubuntu hardy partner' state=absent
 
-# On Ubuntu target: add nginx stable repository from PPA and install its signing key.
-# On Debian target: adding PPA is not available, so it will fail immediately.
+# Add nginx stable repository from PPA and install its signing key.
+# On Ubuntu target:
 apt_repository: repo='ppa:nginx/stable'
+# On Debian target
+apt_repository: repo='ppa:nginx/stable' codename='trusty'
 '''
 
 import glob
@@ -375,6 +382,7 @@ class UbuntuSourcesList(SourcesList):
     def __init__(self, module, add_ppa_signing_keys_callback=None):
         self.module = module
         self.add_ppa_signing_keys_callback = add_ppa_signing_keys_callback
+        self.codename = module.params['codename'] or distro.codename
         super(UbuntuSourcesList, self).__init__(module)
 
     def _get_ppa_info(self, owner_name, ppa_name):
@@ -394,7 +402,7 @@ def _expand_ppa(self, path):
         except IndexError:
             ppa_name = 'ppa'
 
-        line = 'deb http://ppa.launchpad.net/%s/%s/ubuntu %s main' % (ppa_owner, ppa_name, distro.codename)
+        line = 'deb http://ppa.launchpad.net/%s/%s/ubuntu %s main' % (ppa_owner, ppa_name, self.codename)
         return line, ppa_owner, ppa_name
 
     def _key_already_exists(self, key_fingerprint):
@@ -415,7 +423,7 @@ def add_source(self, line, comment='', file=None):
                     command = ['apt-key', 'adv', '--recv-keys', '--keyserver', 'hkp://keyserver.ubuntu.com:80', info['signing_key_fingerprint']]
                     self.add_ppa_signing_keys_callback(command)
 
-            file = file or self._suggest_filename('%s_%s' % (line, distro.codename))
+            file = file or self._suggest_filename('%s_%s' % (line, self.codename))
         else:
             source = self._parse(line, raise_if_invalid_or_disabled=True)[2]
             file = file or self._suggest_filename(source)
@@ -469,6 +477,7 @@ def main():
             # this should not be needed, but exists as a failsafe
             install_python_apt=dict(required=False, default="yes", type='bool'),
             validate_certs = dict(default='yes', type='bool'),
+            codename = dict(required=False),
         ),
         supports_check_mode=True,
     )
@@ -487,13 +496,11 @@ def main():
         else:
             module.fail_json(msg='%s is not installed, and install_python_apt is False' % PYTHON_APT)
 
-    if isinstance(distro, aptsources_distro.UbuntuDistribution):
+    if isinstance(distro, aptsources_distro.Distribution):
         sourceslist = UbuntuSourcesList(module,
             add_ppa_signing_keys_callback=get_add_ppa_signing_key_callback(module))
-    elif isinstance(distro, aptsources_distro.Distribution):
-        sourceslist = SourcesList(module)
     else:
-        module.fail_json(msg='Module apt_repository supports only Debian and Ubuntu.')
+        module.fail_json(msg='Module apt_repository is not supported on target.')
 
     sources_before = sourceslist.dump()
 


Test patch: