1#!/usr/local/bin/python3.8
2# -*- coding: utf-8 -*-
3
4# This module is proudly sponsored by CGI (www.cgi.com) and
5# KPN (www.kpn.com).
6# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
7
8from __future__ import absolute_import, division, print_function
9__metaclass__ = type
10
11
12DOCUMENTATION = '''
13---
14module: icinga2_host
15short_description: Manage a host in Icinga2
16description:
17   - "Add or remove a host to Icinga2 through the API."
18   - "See U(https://www.icinga.com/docs/icinga2/latest/doc/12-icinga2-api/)"
19author: "Jurgen Brand (@t794104)"
20options:
21  url:
22    type: str
23    description:
24      - HTTP, HTTPS, or FTP URL in the form (http|https|ftp)://[user[:pass]]@host.domain[:port]/path
25  use_proxy:
26    description:
27      - If C(no), it will not use a proxy, even if one is defined in
28        an environment variable on the target hosts.
29    type: bool
30    default: 'yes'
31  validate_certs:
32    description:
33      - If C(no), SSL certificates will not be validated. This should only be used
34        on personally controlled sites using self-signed certificates.
35    type: bool
36    default: 'yes'
37  url_username:
38    type: str
39    description:
40      - The username for use in HTTP basic authentication.
41      - This parameter can be used without C(url_password) for sites that allow empty passwords.
42  url_password:
43    type: str
44    description:
45        - The password for use in HTTP basic authentication.
46        - If the C(url_username) parameter is not specified, the C(url_password) parameter will not be used.
47  force_basic_auth:
48    description:
49      - httplib2, the library used by the uri module only sends authentication information when a webservice
50        responds to an initial request with a 401 status. Since some basic auth services do not properly
51        send a 401, logins will fail. This option forces the sending of the Basic authentication header
52        upon initial request.
53    type: bool
54    default: 'no'
55  client_cert:
56    type: path
57    description:
58      - PEM formatted certificate chain file to be used for SSL client
59        authentication. This file can also include the key as well, and if
60        the key is included, C(client_key) is not required.
61  client_key:
62    type: path
63    description:
64      - PEM formatted file that contains your private key to be used for SSL
65        client authentication. If C(client_cert) contains both the certificate
66        and key, this option is not required.
67  state:
68    type: str
69    description:
70      - Apply feature state.
71    choices: [ "present", "absent" ]
72    default: present
73  name:
74    type: str
75    description:
76      - Name used to create / delete the host. This does not need to be the FQDN, but does needs to be unique.
77    required: true
78    aliases: [host]
79  zone:
80    type: str
81    description:
82      - The zone from where this host should be polled.
83  template:
84    type: str
85    description:
86      - The template used to define the host.
87      - Template cannot be modified after object creation.
88  check_command:
89    type: str
90    description:
91      - The command used to check if the host is alive.
92    default: "hostalive"
93  display_name:
94    type: str
95    description:
96      - The name used to display the host.
97      - If not specified, it defaults to the value of the I(name) parameter.
98  ip:
99    type: str
100    description:
101      - The IP address of the host.
102    required: true
103  variables:
104    type: dict
105    description:
106      - Dictionary of variables.
107extends_documentation_fragment:
108  - url
109'''
110
111EXAMPLES = '''
112- name: Add host to icinga
113  community.general.icinga2_host:
114    url: "https://icinga2.example.com"
115    url_username: "ansible"
116    url_password: "a_secret"
117    state: present
118    name: "{{ ansible_fqdn }}"
119    ip: "{{ ansible_default_ipv4.address }}"
120    variables:
121      foo: "bar"
122  delegate_to: 127.0.0.1
123'''
124
125RETURN = '''
126name:
127    description: The name used to create, modify or delete the host
128    type: str
129    returned: always
130data:
131    description: The data structure used for create, modify or delete of the host
132    type: dict
133    returned: always
134'''
135
136import json
137import os
138
139from ansible.module_utils.basic import AnsibleModule
140from ansible.module_utils.urls import fetch_url, url_argument_spec
141
142
143# ===========================================
144# Icinga2 API class
145#
146class icinga2_api:
147    module = None
148
149    def __init__(self, module):
150        self.module = module
151
152    def call_url(self, path, data='', method='GET'):
153        headers = {
154            'Accept': 'application/json',
155            'X-HTTP-Method-Override': method,
156        }
157        url = self.module.params.get("url") + "/" + path
158        rsp, info = fetch_url(module=self.module, url=url, data=data, headers=headers, method=method, use_proxy=self.module.params['use_proxy'])
159        body = ''
160        if rsp:
161            body = json.loads(rsp.read())
162        if info['status'] >= 400:
163            body = info['body']
164        return {'code': info['status'], 'data': body}
165
166    def check_connection(self):
167        ret = self.call_url('v1/status')
168        if ret['code'] == 200:
169            return True
170        return False
171
172    def exists(self, hostname):
173        data = {
174            "filter": "match(\"" + hostname + "\", host.name)",
175        }
176        ret = self.call_url(
177            path="v1/objects/hosts",
178            data=self.module.jsonify(data)
179        )
180        if ret['code'] == 200:
181            if len(ret['data']['results']) == 1:
182                return True
183        return False
184
185    def create(self, hostname, data):
186        ret = self.call_url(
187            path="v1/objects/hosts/" + hostname,
188            data=self.module.jsonify(data),
189            method="PUT"
190        )
191        return ret
192
193    def delete(self, hostname):
194        data = {"cascade": 1}
195        ret = self.call_url(
196            path="v1/objects/hosts/" + hostname,
197            data=self.module.jsonify(data),
198            method="DELETE"
199        )
200        return ret
201
202    def modify(self, hostname, data):
203        ret = self.call_url(
204            path="v1/objects/hosts/" + hostname,
205            data=self.module.jsonify(data),
206            method="POST"
207        )
208        return ret
209
210    def diff(self, hostname, data):
211        ret = self.call_url(
212            path="v1/objects/hosts/" + hostname,
213            method="GET"
214        )
215        changed = False
216        ic_data = ret['data']['results'][0]
217        for key in data['attrs']:
218            if key not in ic_data['attrs'].keys():
219                changed = True
220            elif data['attrs'][key] != ic_data['attrs'][key]:
221                changed = True
222        return changed
223
224
225# ===========================================
226# Module execution.
227#
228def main():
229    # use the predefined argument spec for url
230    argument_spec = url_argument_spec()
231    # add our own arguments
232    argument_spec.update(
233        state=dict(default="present", choices=["absent", "present"]),
234        name=dict(required=True, aliases=['host']),
235        zone=dict(),
236        template=dict(default=None),
237        check_command=dict(default="hostalive"),
238        display_name=dict(default=None),
239        ip=dict(required=True),
240        variables=dict(type='dict', default=None),
241    )
242
243    # Define the main module
244    module = AnsibleModule(
245        argument_spec=argument_spec,
246        supports_check_mode=True
247    )
248
249    state = module.params["state"]
250    name = module.params["name"]
251    zone = module.params["zone"]
252    template = [name]
253    if module.params["template"]:
254        template.append(module.params["template"])
255    check_command = module.params["check_command"]
256    ip = module.params["ip"]
257    display_name = module.params["display_name"]
258    if not display_name:
259        display_name = name
260    variables = module.params["variables"]
261
262    try:
263        icinga = icinga2_api(module=module)
264        icinga.check_connection()
265    except Exception as e:
266        module.fail_json(msg="unable to connect to Icinga. Exception message: %s" % (e))
267
268    data = {
269        'attrs': {
270            'address': ip,
271            'display_name': display_name,
272            'check_command': check_command,
273            'zone': zone,
274            'vars': {
275                'made_by': "ansible",
276            },
277            'templates': template,
278        }
279    }
280
281    if variables:
282        data['attrs']['vars'].update(variables)
283
284    changed = False
285    if icinga.exists(name):
286        if state == "absent":
287            if module.check_mode:
288                module.exit_json(changed=True, name=name, data=data)
289            else:
290                try:
291                    ret = icinga.delete(name)
292                    if ret['code'] == 200:
293                        changed = True
294                    else:
295                        module.fail_json(msg="bad return code (%s) deleting host: '%s'" % (ret['code'], ret['data']))
296                except Exception as e:
297                    module.fail_json(msg="exception deleting host: " + str(e))
298
299        elif icinga.diff(name, data):
300            if module.check_mode:
301                module.exit_json(changed=False, name=name, data=data)
302
303            # Template attribute is not allowed in modification
304            del data['attrs']['templates']
305
306            ret = icinga.modify(name, data)
307
308            if ret['code'] == 200:
309                changed = True
310            else:
311                module.fail_json(msg="bad return code (%s) modifying host: '%s'" % (ret['code'], ret['data']))
312
313    else:
314        if state == "present":
315            if module.check_mode:
316                changed = True
317            else:
318                try:
319                    ret = icinga.create(name, data)
320                    if ret['code'] == 200:
321                        changed = True
322                    else:
323                        module.fail_json(msg="bad return code (%s) creating host: '%s'" % (ret['code'], ret['data']))
324                except Exception as e:
325                    module.fail_json(msg="exception creating host: " + str(e))
326
327    module.exit_json(changed=changed, name=name, data=data)
328
329
330# import module snippets
331if __name__ == '__main__':
332    main()
333