Repository: ansible-collections/community.general

Issue:
Module haproxy times out instead setting HAProxy backends into DRAIN mode followed by MAINT mode
**Summary**

HAProxy allows to set backends into DRAIN mode that waits until the server has no active connections anymore before setting the backends into MAINT mode.

The respective Ansible haproxy module has a state and parameter `drain` that can be used to do this with Ansible.

At the moment and given my setup the module haproxy never enters MAINT mode, times out and fails.

Please note that this has already been reported with a suggested fix here but has been closed during migration to this new repository:

* Issue: https://github.com/ansible/ansible/issues/37591
* Pull Request: https://github.com/ansible/ansible/pull/59156

Reopening it here in the new repository for Ansible collection "community.general".

**Issue Type**

Bug Report

**Component Name**

haproxy

**Ansible Version**

```paste below
ansible 2.10.6
  config file = /home/xxx/yyyy/ansible.cfg
  configured module search path = ['/home/xxx/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /home/xxx/.local/share/virtualenvs/ansible-_MQ25oBn/lib/python3.8/site-packages/ansible
  executable location = /home/xxx/.local/share/virtualenvs/ansible-_MQ25oBn/bin/ansible
  python version = 3.8.0 (default, Oct 28 2019, 16:14:01) [GCC 8.3.0]
```


**Configuration**

```paste below
ANSIBLE_PIPELINING(/home/xxx/yy/ansible.cfg) = True
ANSIBLE_SSH_ARGS(/home/xxx/yyy/ansible.cfg) = -C -o ControlMaster=auto -o ControlPersist=5m
COLLECTIONS_PATHS(/home/xxx/yyy/ansible.cfg) = ['/home/xxx/yyy/collections']
DEFAULT_HOST_LIST(/home/xxx/yyy/ansible.cfg) = ['/home/xxx/yyy/inventories/development/hosts']
DEFAULT_REMOTE_PORT(/home/xxx/yyy/ansible.cfg) = 2222
DEFAULT_REMOTE_USER(/home/xxx/yyy/ansible.cfg) = root
DEFAULT_ROLES_PATH(/home/xxx/yyy/ansible.cfg) = ['/home/hxxx/yyy/roles']
DEFAULT_SCP_IF_SSH(/home/xxx/yyy/ansible.cfg) = True
INTERPRETER_PYTHON(/home/xxx/yyy/ansible.cfg) = auto
RETRY_FILES_ENABLED(/home/xxx/yyy/ansible.cfg) = False
```


**OS / Environment**

Ubuntu 18.04.5 LTS


**Steps To Reproduce**

The following three cases reproduce the same bug:

1)

```yaml
- name: Drain HAProxy Backends
  community.general.haproxy:
    host: "hostname"
    state: drain
    socket: "/run/haproxy/admin.sock"
    backend: "backend"
    wait: yes
    wait_interval: 5
    wait_retries: 60
- name: Disable HAProxy Backends
  community.general.haproxy:
    host: "hostname"
    state: disabled
    socket: "/run/haproxy/admin.sock"
    backend: "backend"
    drain: yes
    wait: yes
    wait_interval: 5
    wait_retries: 60
```

2)

```yaml
- name: Drain HAProxy Backends
  community.general.haproxy:
    host: "hostname"
    state: drain
    socket: "/run/haproxy/admin.sock"
    backend: "backend"
- name: Disable HAProxy Backends
  community.general.haproxy:
    host: "hostname"
    state: disabled
    socket: "/run/haproxy/admin.sock"
    backend: "backend"
    drain: yes
    wait: yes
    wait_interval: 5
    wait_retries: 60
```

3)

```yaml
- name: Drain and disable HAProxy Backends
  community.general.haproxy:
    host: "hostname"
    state: disabled
    socket: "/run/haproxy/admin.sock"
    backend: "backend"
    drain: yes
    wait: yes
    wait_interval: 5
    wait_retries: 60
```

**Expected Results**

It is expected that Ansible sets the respective HAProxy backends first in DRAIN mode and after that in MAINT mode if the server has no active connections anymore.


**Actual Results**

Ansible module haproxy waits until timeout is reached and fails because mode MAINT is never reached for case 1 mentioned above:

```paste below
TASK [Drain HAProxy Backends] ******************************************************************************************************************************************
changed: {"changed": true, "command": ["set server backend/hostname state drain"], "output": [""], "state_after": [{"scur": "0", "status": "DRAIN", "weight": "1"}], "state_before": [{"scur": "0", "status": "UP", "weight": "1"}]}                                              
                                   
TASK [Disable HAProxy Backends] ******************************************************************************************************************************************
failed: {"changed": false, "msg": "server backend/hostname not status 'MAINT' after 60 retries. Aborting."}
```





Discussion:
Files identified in the description:
* [`plugins/modules/net_tools/haproxy.py`](https://github.com/ansible-collections/community.general/blob/master/plugins/modules/net_tools/haproxy.py)

If these files are inaccurate, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
cc @nerzhul @ravibhure
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->

Resolving patch:
diff --git a/plugins/modules/net_tools/haproxy.py b/plugins/modules/net_tools/haproxy.py
--- a/plugins/modules/net_tools/haproxy.py
+++ b/plugins/modules/net_tools/haproxy.py
@@ -367,10 +367,9 @@ def wait_until_status(self, pxname, svname, status):
             # We can assume there will only be 1 element in state because both svname and pxname are always set when we get here
             # When using track we get a status like this: MAINT (via pxname/svname) so we need to do substring matching
             if status in state[0]['status']:
-                if not self._drain or (state[0]['scur'] == '0' and 'MAINT' in state):
+                if not self._drain or state[0]['scur'] == '0':
                     return True
-            else:
-                time.sleep(self.wait_interval)
+            time.sleep(self.wait_interval)
 
         self.module.fail_json(msg="server %s/%s not status '%s' after %d retries. Aborting." %
                               (pxname, svname, status, self.wait_retries))
@@ -409,15 +408,17 @@ def disabled(self, host, backend, shutdown_sessions):
     def drain(self, host, backend, status='DRAIN'):
         """
         Drain action, sets the server to DRAIN mode.
-        In this mode mode, the server will not accept any new connections
+        In this mode, the server will not accept any new connections
         other than those that are accepted via persistence.
         """
         haproxy_version = self.discover_version()
 
-        # check if haproxy version suppots DRAIN state (starting with 1.5)
+        # check if haproxy version supports DRAIN state (starting with 1.5)
         if haproxy_version and (1, 5) <= haproxy_version:
             cmd = "set server $pxname/$svname state drain"
-            self.execute_for_backends(cmd, backend, host, status)
+            self.execute_for_backends(cmd, backend, host, "DRAIN")
+            if status == "MAINT":
+                self.disabled(host, backend, self.shutdown_sessions)
 
     def act(self):
         """
@@ -426,7 +427,7 @@ def act(self):
         # Get the state before the run
         self.command_results['state_before'] = self.get_state_for(self.backend, self.host)
 
-        # toggle enable/disbale server
+        # toggle enable/disable server
         if self.state == 'enabled':
             self.enabled(self.host, self.backend, self.weight)
         elif self.state == 'disabled' and self._drain:


Test patch: