Repository: ansible-collections/community.general

Issue:
Add support for TCP healthcheck for services registered by consul module
<!--- Verify first that your feature was not already discussed on GitHub -->
<!--- Complete *all* sections as described, this form is processed automatically -->

##### SUMMARY
<!--- Describe the new feature/improvement briefly below -->
Consul provides a TCP check type and this should be part of this module
https://www.consul.io/docs/discovery/checks

##### ISSUE TYPE
- Feature Idea

##### COMPONENT NAME
consul - https://docs.ansible.com/ansible/latest/collections/community/general/consul_module.html

##### ADDITIONAL INFORMATION
<!--- Describe how the feature would be used, why it is needed and what it would solve -->
Current workaround is: `nc -z -v -w5 127.0.0.1 80`
Old issue - https://github.com/ansible/ansible/issues/25801
And closed PR for this - https://github.com/ansible/ansible/pull/46748

<!--- Paste example playbooks or commands between quotes below -->
This would look like this:
```yaml
- name: Register nginx with an tcp check
  community.general.consul:
    service_name: nginx
    service_port: 80
    interval: 60s
    tcp: 172.34.12.49:80
```

<!--- HINT: You can also paste gist.github.com links for larger files -->



Discussion:
Files identified in the description:
None

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 --->
!component =plugins/modules/clustering/consul/consul.py
Files identified in the description:
* [`plugins/modules/clustering/consul/consul.py`](https://github.com/ansible-collections/community.general/blob/master/plugins/modules/clustering/consul/consul.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 @colin-nolan @sgargan
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->

Resolving patch:
diff --git a/plugins/modules/clustering/consul/consul.py b/plugins/modules/clustering/consul/consul.py
--- a/plugins/modules/clustering/consul/consul.py
+++ b/plugins/modules/clustering/consul/consul.py
@@ -120,6 +120,14 @@
             Similar to the interval this is a number with a s or m suffix to
             signify the units of seconds or minutes e.g C(15s) or C(1m). If no suffix
             is supplied, C(m) will be used by default e.g. C(1) will be C(1m)
+    tcp:
+        type: str
+        description:
+          - Checks can be registered with a TCP port. This means that consul
+            will check if the connection attempt to that port is successful (that is, the port is currently accepting connections).
+            The format is C(host:port), for example C(localhost:80).
+            I(interval) must also be provided with this option.
+        version_added: '1.3.0'
     http:
         type: str
         description:
@@ -151,6 +159,13 @@
     script: curl http://localhost
     interval: 60s
 
+- name: register nginx with a tcp check
+  community.general.consul:
+    service_name: nginx
+    service_port: 80
+    interval: 60s
+    tcp: localhost:80
+
 - name: Register nginx with an http check
   community.general.consul:
     service_name: nginx
@@ -217,6 +232,7 @@ def deregister(self, service_id, token=None):
 except ImportError:
     python_consul_installed = False
 
+import re
 from ansible.module_utils.basic import AnsibleModule
 
 
@@ -274,6 +290,7 @@ def add_check(module, check):
                      script=check.script,
                      interval=check.interval,
                      ttl=check.ttl,
+                     tcp=check.tcp,
                      http=check.http,
                      timeout=check.timeout,
                      service_id=check.service_id)
@@ -346,11 +363,11 @@ def get_service_by_id_or_name(consul_api, service_id_or_name):
 
 
 def parse_check(module):
-    if len([p for p in (module.params.get('script'), module.params.get('ttl'), module.params.get('http')) if p]) > 1:
+    if len([p for p in (module.params.get('script'), module.params.get('ttl'), module.params.get('tcp'), module.params.get('http')) if p]) > 1:
         module.fail_json(
-            msg='checks are either script, http or ttl driven, supplying more than one does not make sense')
+            msg='checks are either script, tcp, http or ttl driven, supplying more than one does not make sense')
 
-    if module.params.get('check_id') or module.params.get('script') or module.params.get('ttl') or module.params.get('http'):
+    if module.params.get('check_id') or module.params.get('script') or module.params.get('ttl') or module.params.get('tcp') or module.params.get('http'):
 
         return ConsulCheck(
             module.params.get('check_id'),
@@ -361,6 +378,7 @@ def parse_check(module):
             module.params.get('interval'),
             module.params.get('ttl'),
             module.params.get('notes'),
+            module.params.get('tcp'),
             module.params.get('http'),
             module.params.get('timeout'),
             module.params.get('service_id'),
@@ -446,7 +464,7 @@ def to_dict(self):
 class ConsulCheck(object):
 
     def __init__(self, check_id, name, node=None, host='localhost',
-                 script=None, interval=None, ttl=None, notes=None, http=None, timeout=None, service_id=None):
+                 script=None, interval=None, ttl=None, notes=None, tcp=None, http=None, timeout=None, service_id=None):
         self.check_id = self.name = name
         if check_id:
             self.check_id = check_id
@@ -458,6 +476,7 @@ def __init__(self, check_id, name, node=None, host='localhost',
         self.interval = self.validate_duration('interval', interval)
         self.ttl = self.validate_duration('ttl', ttl)
         self.script = script
+        self.tcp = tcp
         self.http = http
         self.timeout = self.validate_duration('timeout', timeout)
 
@@ -475,6 +494,18 @@ def __init__(self, check_id, name, node=None, host='localhost',
 
             self.check = consul.Check.http(http, self.interval, self.timeout)
 
+        if tcp:
+            if interval is None:
+                raise Exception('tcp check must specify interval')
+
+            regex = r"(?P<host>.*)(?::)(?P<port>(?:[0-9]+))$"
+            match = re.match(regex, tcp)
+
+            if match is None:
+                raise Exception('tcp check must be in host:port format')
+
+            self.check = consul.Check.tcp(match.group('host').strip('[]'), int(match.group('port')), self.interval)
+
     def validate_duration(self, name, duration):
         if duration:
             duration_units = ['ns', 'us', 'ms', 's', 'm', 'h']
@@ -508,6 +539,7 @@ def to_dict(self):
         self._add(data, 'host')
         self._add(data, 'interval')
         self._add(data, 'ttl')
+        self._add(data, 'tcp')
         self._add(data, 'http')
         self._add(data, 'timeout')
         self._add(data, 'service_id')
@@ -547,6 +579,7 @@ def main():
             state=dict(default='present', choices=['present', 'absent']),
             interval=dict(required=False, type='str'),
             ttl=dict(required=False, type='str'),
+            tcp=dict(required=False, type='str'),
             http=dict(required=False, type='str'),
             timeout=dict(required=False, type='str'),
             tags=dict(required=False, type='list'),


Test patch: