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