Repository: ansible/ansible

Issue:
Apache Enable Module... Module fails if already enabled

Verify first that your issue/request is not already reported on GitHub.
Also test if the latest release, and master branch are affected too.
>>> DONE , using latest version from pip... will try and use latest from source.


##### ISSUE TYPE
<!--- Pick one below and delete the rest: -->
 - Bug Report


##### COMPONENT NAME
http://docs.ansible.com/ansible/apache2_module_module.html

##### ANSIBLE VERSION
<!--- Paste verbatim output from “ansible --version” between quotes below -->
```
ansible 2.2.1.0
  config file =
  configured module search path = Default w/o overrides

```

##### CONFIGURATION
<!---
>> DEFAULT
-->

##### OS / ENVIRONMENT
<!---
Distributor ID: Ubuntu
Description:    Ubuntu 16.04.1 LTS
Release:        16.04
Codename:       xenial

-->

##### SUMMARY
Ansible apache module fails if evasive is already enabled, only seems to be evasive.


##### STEPS TO REPRODUCE
<!---
For bugs, show exactly how to reproduce the problem, using a minimal test-case.
For new features, show how the feature would be used.
-->

<!--- Paste example playbooks or commands between quotes below -->
[https://github.com/ShadeySecurity/pyAssetContext/blob/master/playbooks/install-pyAssetContext.yml](url)

<!--- You can also paste gist.github.com links for larger files -->

##### EXPECTED RESULTS
<!--- What did you expect to happen when running the steps above? -->
I expected an ok / no change
##### ACTUAL RESULTS
<!--- What actually happened? If possible run with extra verbosity (-vvvv) -->
Failed due to already existing
<!--- Paste verbatim command output between quotes below -->
```
TASK [Apache Modules Setup] ****************************************************
ok: [127.0.0.1] => (item=ssl)
ok: [127.0.0.1] => (item=proxy)
ok: [127.0.0.1] => (item=proxy_http)
ok: [127.0.0.1] => (item=headers)
ok: [127.0.0.1] => (item=expires)
ok: [127.0.0.1] => (item=wsgi)
ok: [127.0.0.1] => (item=rewrite)
ok: [127.0.0.1] => (item=security2)
...ignoring
failed: [127.0.0.1] (item=evasive) => {"failed": true, "item": "evasive", "msg": "Failed to set module evasive to enabled: Module evasive already enabled\n", "rc": 0, "stderr": "", "stdout": "Module evasive already enabled\n", "stdout_lines": ["Module evasive already enabled"]}
```



Discussion:
cc @robinro @n0trax
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->
@theshades Thanks for this bug report.

Which OS/version are you using?

I can reproduce the issue with ubuntu 16.04. The reason is that `apache2ctl -M` outputs `evasive20_module` instead of `evasive_module`, which breaks this module. 

This problem is similar to 
https://github.com/ansible/ansible-modules-core/issues/5779, https://github.com/ansible/ansible-modules-core/issues/4744, https://github.com/ansible/ansible/issues/20288 

I'd ask you to file a bug report against the distribution that you are using and ask them to fix that output, since it obviously deviates from the default that the vast majority of modules (and all others that you use) follow. Please link to that bug report here.

We could construct a workaround here, like in https://github.com/ansible/ansible/pull/20341. I'm not really happy with that, but we starting to go down the "workaround" road and there is no good argument why to stop now. @n0trax What's your view on that?
@theshades Thanks for this bug report.

I can reproduce this issue too...

@robinro I agree with you. 

Resolving patch:
diff --git a/lib/ansible/modules/web_infrastructure/apache2_module.py b/lib/ansible/modules/web_infrastructure/apache2_module.py
--- a/lib/ansible/modules/web_infrastructure/apache2_module.py
+++ b/lib/ansible/modules/web_infrastructure/apache2_module.py
@@ -109,10 +109,7 @@ def _run_threaded(module):
 
     result, stdout, stderr = module.run_command("%s -V" % control_binary)
 
-    if re.search(r'threaded:[ ]*yes', stdout):
-        return True
-    else:
-        return False
+    return bool(re.search(r'threaded:[ ]*yes', stdout))
 
 def _get_ctl_binary(module):
     for command in ['apache2ctl', 'apachectl']:
@@ -146,20 +143,38 @@ def _module_is_enabled(module):
         else:
             module.fail_json(msg=error_msg)
 
-    """
-    Work around for php modules; php7.x are always listed as php7_module
-    """
-    php_module = re.search(r'^(php\d)\.', name)
-    if php_module:
-        name = php_module.group(1)
+    searchstring = ' ' + create_apache_identifier(name)
+    return searchstring in stdout
 
+def create_apache_identifier(name):
     """
-    Workaround for shib2; module is listed as mod_shib
-    """
-    if re.search(r'shib2', name):
-        return bool(re.search(r' mod_shib', stdout))
+    By convention if a module is loaded via name, it appears in apache2ctl -M as
+    name_module.
+
+    Some modules don't follow this convention and we use replacements for those."""
+
+    # a2enmod name replacement to apache2ctl -M names
+    text_workarounds = [
+        ('shib2', 'mod_shib'),
+        ('evasive', 'evasive20_module'),
+    ]
+
+    # re expressions to extract subparts of names
+    re_workarounds = [
+        ('php', r'^(php\d)\.'),
+    ]
+
+    for a2enmod_spelling, module_name in text_workarounds:
+        if a2enmod_spelling in name:
+            return module_name
+
+    for search, reexpr in re_workarounds:
+        if search in name:
+            rematch = re.search(reexpr, name)
+            return rematch.group(1) + '_module'
+
+    return name + '_module'
 
-    return bool(re.search(r' ' + name + r'_module', stdout))
 
 def _set_state(module, state):
     name = module.params['name']
@@ -221,6 +236,6 @@ def main():
         _set_state(module, module.params['state'])
 
 # import module snippets
-from ansible.module_utils.basic import *
+from ansible.module_utils.basic import AnsibleModule
 if __name__ == '__main__':
     main()


Test patch:
diff --git a/test/integration/targets/apache2_module/tasks/actualtest.yml b/test/integration/targets/apache2_module/tasks/actualtest.yml
--- a/test/integration/targets/apache2_module/tasks/actualtest.yml
+++ b/test/integration/targets/apache2_module/tasks/actualtest.yml
@@ -15,9 +15,12 @@
 
 - name: install apache via apt
   apt:
-    name: apache2
+    name: "{{item}}"
     state: present
   when: "ansible_os_family == 'Debian'"
+  with_items:
+    - apache2
+    - libapache2-mod-evasive
 
 - name: install apache via zypper
   zypper:
@@ -84,4 +87,11 @@
     name: autoindex
     state: absent
     force: True
-  when: "ansible_os_family != 'Suse'"
+  when: "ansible_os_family == 'Debian'"
+
+
+- name: enable evasive module, test https://github.com/ansible/ansible/issues/22635
+  apache2_module:
+    name: evasive
+    state: present
+  when: "ansible_os_family == 'Debian'"
diff --git a/test/units/modules/web_infrastructure/test_apache2_module.py b/test/units/modules/web_infrastructure/test_apache2_module.py
new file mode 100644
--- /dev/null
+++ b/test/units/modules/web_infrastructure/test_apache2_module.py
@@ -0,0 +1,16 @@
+import pytest
+
+from ansible.modules.web_infrastructure.apache2_module import create_apache_identifier
+
+REPLACEMENTS = [
+    ('php7.1', 'php7_module'),
+    ('php5.6', 'php5_module'),
+    ('shib2', 'mod_shib'),
+    ('evasive', 'evasive20_module'),
+    ('thismoduledoesnotexist', 'thismoduledoesnotexist_module'),  # the default
+]
+
+@pytest.mark.parametrize("replacement", REPLACEMENTS, ids=lambda x: x[0])
+def test_apache_identifier(replacement):
+    "test the correct replacement of an a2enmod name with an apache2ctl name"
+    assert create_apache_identifier(replacement[0]) == replacement[1]