Repository: ansible/ansible-modules-extras

Issue:
Consul module does not allow for more than one check per service
##### ISSUE TYPE
- Bug Report
##### COMPONENT NAME

 ansible-modules-extras/clustering/consul.py
##### ANSIBLE VERSION

```
ansible 2.1.2.0
  config file = 
  configured module search path = Default w/o overrides
```
##### CONFIGURATION

N/A
##### OS / ENVIRONMENT

N/A
##### SUMMARY

Consul supports healthchecks at the node (host) level, and also explicitly against specific services on the host.
When creating these healthchecks using Consul's HTTP API, you have a few options, including:
1. Register a service, and specify a healthcheck as part of that registration.
2. Explicitly register a check at the node level.
3. Explicitly register a check against an existing service on a node.

Using point 3 above, it is possible to have multiple healthchecks registered for a service.

The Ansible consul module currently does not support this. If a `service_id` is specified when registering a check, it is ignored, and the check becomes like point 2 above (registered at the Node level).

This is an inconsistency between the Ansible consul module and the Consul HTTP API. It is also inconsistent with Consul libraries in other languages (Node.js and Golang both definitely support this).
##### STEPS TO REPRODUCE

Tested with local standalone consul instance. Run playbook below 3 times, to cover the 3 `TEST` tasks - commenting out the other two as needed each time.

Consul being run via Docker:
`docker run -p 8400:8400 -p 8500:8500 -p 8600:53/udp -h node1 progrium/consul -server -bootstrap -ui-dir /ui`
Status then viewed in the Consul ui: http://localhost:8500/ui/#/dc1/services

Playbook:

```

---
- hosts: localhost
  vars:
    testhost: "localhost"
    testservice: "jhart_test"
    testport: "9999"
    testsuffix1: "status1"
    testsuffix2: "status2"
  tasks:
  - name: clean up any old registration
    consul:
      host: "{{ testhost }}"
      service_name: "{{ testservice }}"
      state: absent
  - name: clean up spurious host level check created by TEST1
    consul:
      host: "{{ testhost }}"
      check_name: "{{ testservice }}-health2"
      state: absent

  - name: register in consul with first http check
    consul:
      host: "{{ testhost }}"
      service_name: "{{ testservice }}"
      service_id: "{{ testservice }}"
      service_address: "{{ testhost }}"
      service_port: "{{ testport }}"
      # As an aside - check_name and check_id are ignored here. The check ends up being called "Service '{{ testservice }}' check"
      check_name: "{{ testservice }}-health1"
      check_id: "{{ testservice }}-health1"
      interval: "10s"
      http: "http://{{ testhost }}:{{ testport }}/{{ testsuffix1 }}"

# TEST1
  - name: register a second check against this service_name
    consul:
      host: "{{ testhost }}"
      # service_id is ignored, and the check is registered at the node level
      service_id: "{{ testservice }}"
      check_name: "{{ testservice }}-health2"
      check_id: "{{ testservice }}-health2"
      interval: "10s"
      http: "http://{{ testhost }}:{{ testport }}/{{ testsuffix2 }}"

# TEST2
  # - name: register a second check against this service_name, take 2
  #   consul:
  #     host: "{{ testhost }}"
  #     # This one fails with:
  #     # fatal: [localhost]: FAILED! => {"changed": false, "failed": true, "msg": "service_name supplied but no service_port, a port is required to configure a service. Did you configure the 'port' argument meaning 'service_port'?"}
  #     service_id: "{{ testservice }}"
  #     service_name: "{{ testservice }}"
  #     check_name: "{{ testservice }}-health2"
  #     check_id: "{{ testservice }}-health2"
  #     interval: "10s"
  #     http: "http://{{ testhost }}:{{ testport }}/{{ testsuffix2 }}"

# TEST3
  # - name: working second check using curl
  #   uri:
  #     url: "http://{{ testhost }}:8500/v1/agent/check/register"
  #     method: PUT
  #     body: "{\"Name\":\"{{ testservice }}-health2\", 
  #     \"ID\":\"{{ testservice }}-health2\", 
  #     \"HTTP\":\"http://{{ testhost }}:{{ testport }}/{{ testsuffix2 }}\", 
  #     \"interval\": \"10s\", 
  #     \"ServiceID\":\"{{ testservice }}\"}"
  #     body_format: json

```
##### EXPECTED RESULTS

TEST1 should be equivalent to TEST3, and should result in the second check being created against the service 'jhart_test'
TEST2 is probably ok failing as it does now. Really included in the playbook for completeness.
##### ACTUAL RESULTS

Playbook output is irrelevant here. TEST1 and TEST3 both run just fine from an Ansible perspective.
BUT - TEST1 results in the check `jhart_test-health2` being created at the node level, rather than against the service.

I would expect TEST1 and TEST3 to produce identical results.



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

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

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


Resolving patch:
diff --git a/clustering/consul.py b/clustering/consul.py
--- a/clustering/consul.py
+++ b/clustering/consul.py
@@ -185,7 +185,7 @@
       service_name: nginx
       service_port: 80
       interval: 60s
-      http: /status
+      http: "http://localhost:80/status"
 
   - name: register external service nginx available at 10.1.5.23
     consul:
@@ -213,6 +213,14 @@
       script: "/opt/disk_usage.py"
       interval: 5m
 
+  - name: register an http check against a service that's already registered
+    consul:
+      check_name: nginx-check2
+      check_id: nginx-check2
+      service_id: nginx
+      interval: 60s
+      http: "http://localhost:80/morestatus"
+
 '''
 
 try:
@@ -265,7 +273,7 @@ def add_check(module, check):
     retrieve the full metadata of an existing check  through the consul api.
     Without this we can't compare to the supplied check and so we must assume
     a change. '''
-    if not check.name:
+    if not check.name and not service_id:
         module.fail_json(msg='a check name is required for a node level check, one not attached to a service')
 
     consul_api = get_consul_api(module)
@@ -278,7 +286,8 @@ def add_check(module, check):
                      interval=check.interval,
                      ttl=check.ttl,
                      http=check.http,
-                     timeout=check.timeout)
+                     timeout=check.timeout,
+                     service_id=check.service_id)
 
 
 def remove_check(module, check_id):
@@ -363,7 +372,8 @@ def parse_check(module):
             module.params.get('ttl'),
             module.params.get('notes'),
             module.params.get('http'),
-            module.params.get('timeout')
+            module.params.get('timeout'),
+            module.params.get('service_id'),
         )
 
 
@@ -451,10 +461,11 @@ def to_dict(self):
 class ConsulCheck():
 
     def __init__(self, check_id, name, node=None, host='localhost',
-                    script=None, interval=None, ttl=None, notes=None, http=None, timeout=None):
+                    script=None, interval=None, ttl=None, notes=None, http=None, timeout=None, service_id=None):
         self.check_id = self.name = name
         if check_id:
             self.check_id = check_id
+        self.service_id = service_id
         self.notes = notes
         self.node = node
         self.host = host
@@ -488,13 +499,14 @@ def validate_duration(self, name, duration):
         return duration
 
     def register(self, consul_api):
-        consul_api.agent.check.register(self.name, check_id=self.check_id,
+        consul_api.agent.check.register(self.name, check_id=self.check_id, service_id=self.service_id,
                                         notes=self.notes,
                                         check=self.check)
 
     def __eq__(self, other):
         return (isinstance(other, self.__class__)
                 and self.check_id == other.check_id
+                and self.service_id == other.service_id
                 and self.name == other.name
                 and self.script == script
                 and self.interval == interval)
@@ -514,6 +526,7 @@ def to_dict(self):
         self._add(data, 'ttl')
         self._add(data, 'http')
         self._add(data, 'timeout')
+        self._add(data, 'service_id')
         return data
 
     def _add(self, data, key, attr=None):


Test patch: