1# encoding: utf-8 2 3# Nagstamon - Nagios status monitor for your desktop 4# Copyright (C) 2008-2021 Henri Wahl <henri@nagstamon.de> et al. 5# 6# This program is free software; you can redistribute it and/or modify 7# it under the terms of the GNU General Public License as published by 8# the Free Software Foundation; either version 2 of the License, or 9# (at your option) any later version. 10# 11# This program is distributed in the hope that it will be useful, 12# but WITHOUT ANY WARRANTY; without even the implied warranty of 13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14# GNU General Public License for more details. 15# 16# You should have received a copy of the GNU General Public License 17# along with this program; if not, write to the Free Software 18# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA 19 20from Nagstamon.Objects import Result 21from Nagstamon.Objects import GenericHost 22from Nagstamon.Objects import GenericService 23from Nagstamon.Servers.Generic import GenericServer 24from Nagstamon.Config import conf 25 26import logging 27log = logging.getLogger('Livestatus') 28 29import re 30import json 31import socket 32import time 33 34 35def format_timestamp(timestamp): 36 """format unix timestamp""" 37 ts_tuple = time.localtime(timestamp) 38 return time.strftime('%Y-%m-%d %H:%M:%S', ts_tuple) 39 40 41def duration(timestamp): 42 """human representation of a duration""" 43 factors = (60 * 60 * 24, 60 * 60, 60, 1) 44 result = [] 45 diff = time.time() - timestamp 46 for f in factors: 47 x = int(diff / f) 48 result.append(x) 49 diff = diff - x * f 50 return '%02dd %02dh %02dm %02ds' % tuple(result) 51 52 53def service_to_host(data): 54 """create the host data blob from the implicit join data of a service""" 55 result = {} 56 for key in data.keys(): 57 if key.startswith('host_'): 58 result[key[5:]] = data[key] 59 return result 60 61 62class LivestatusServer(GenericServer): 63 """A server running MK Livestatus plugin. Tested with icinga2""" 64 65 TYPE = 'Livestatus' 66 67 def init_config(self): 68 log.info(self.monitor_url) 69 # we abuse the monitor_url for the connection information 70 self.address = ('localhost', 6558) 71 m = re.match('.*?://([^:/]+?)(?::(\d+))?(?:/|$)', self.monitor_url) 72 if m: 73 host, port = m.groups() 74 if not port: 75 port = 6558 76 else: 77 port = int(port) 78 self.address = (host, port) 79 else: 80 log.error('unable to parse monitor_url %s', self.monitor_url) 81 self.enable = False 82 83 def init_HTTP(self): 84 pass 85 86 def communicate(self, data, response=True): 87 buffersize = 2**20 88 data.append('') 89 data.append('') 90 s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 91 log.debug('connecting') 92 s.connect(self.address) 93 s.send('\n'.join(data).encode('utf8')) 94 if not response: 95 log.debug('no response required, disconnect') 96 s.close() 97 return '' 98 result = bytes() 99 line = s.recv(buffersize) 100 while len(line) > 0: 101 result += line 102 line = s.recv(buffersize) 103 log.debug('disconnect') 104 s.close() 105 log.debug('received %d bytes', len(result)) 106 result = result.decode('utf8') 107 return result 108 109 def get(self, table, raw=[], headers={}): 110 """send data to livestatus socket, receive result, format as json""" 111 data = ['GET %s' % table, ] 112 headers['OutputFormat'] = 'json' 113 headers['ColumnHeaders'] = 'on' 114 for k, v in headers.items(): 115 data.append('%s: %s' % (k, v)) 116 for line in raw: 117 data.append(line) 118 result = self.communicate(data) 119 if result: 120 return json.loads(result) 121 return result 122 123 def command(self, *cmd): 124 """execute nagios command via livestatus socket. 125 For commands see 126 https://old.nagios.org/developerinfo/externalcommands/commandlist.php 127 """ 128 data = [] 129 ts = str(int(time.time()) + 5) # current epoch timestamp + 5 seconds 130 for line in cmd: 131 line = 'COMMAND [TIMESTAMP] ' + line 132 data.append(line.replace('TIMESTAMP', ts)) 133 self.communicate(data, response=False) 134 135 def table(self, data): 136 """take a livestatus answer and format it as a table, 137 list of dictionaries 138 [ {host: 'foo1', service: 'bar1'}, {host: 'foo2', service: 'bar2'} ] 139 """ 140 try: 141 header = data[0] 142 except IndexError: 143 raise StopIteration 144 for line in data[1:]: 145 yield(dict(zip(header, line))) 146 147 def _get_status(self): 148 """fetch any host/service not in OK state 149 store the information in self.new_hosts 150 applies basic filtering. All additional 151 filtering and merging new_hosts to hosts 152 is left to nagstamon 153 """ 154 log.debug('_get_status') 155 self.new_hosts = dict() 156 filters = [] 157 filters.append('Filter: state != 0') # ignore OK state 158 if conf.filter_acknowledged_hosts_services: 159 filters.append('Filter: acknowledged != 1') 160 # hosts 161 data = self.get("hosts", raw=filters) 162 for h in self.table(data): 163 host = self._create_host(h) 164 self.new_hosts[host.name] = host 165 log.info("host %s is %s", host.name, host.status) 166 # services 167 data = self.get("services", raw=filters) 168 for s in self.table(data): 169 # service are attached to host objects 170 if s['host_name'] in self.new_hosts: 171 host = self.new_hosts[s['host_name']] 172 else: 173 # need to create the host 174 # icinga2 adds all host information to the server 175 # prefixed with HOST_ 176 xdata = service_to_host(s) # any field starting with HOST_ 177 host = self._create_host(xdata) 178 self.new_hosts[host.name] = host 179 service = self._create_service(s) 180 service.host = host.name 181 host.services[service.name] = service 182 return Result() 183 184 def _update_object(self, obj, data): 185 """populate the generic fields of obj (GenericHost or GenericService) 186 from data.""" 187 result = obj 188 result.server = self.name 189 result.last_check = format_timestamp(data['last_check']) 190 result.duration = duration(data['last_state_change']) 191 result.attempt = data['current_attempt'] 192 result.status_information = data['plugin_output'] 193 result.passiveonly = False 194 result.notifications_disabled = data['notifications_enabled'] != 1 195 result.flapping = data['is_flapping'] == 1 196 result.acknowledged = data['acknowledged'] == 1 197 result.scheduled_downtime = data['scheduled_downtime_depth'] == 1 198 if data['state'] == data['last_hard_state']: 199 result.status_type = 'hard' 200 else: 201 result.status_type = 'soft' 202 return result 203 204 def _create_host(self, data): 205 """create GenericHost from json data""" 206 result = self._update_object(GenericHost(), data) 207 result.name = data['name'] 208 host_states = {0: 'UP', 1: 'DOWN', 2: 'UNKNOWN'} 209 result.status = host_states[data['state']] 210 return result 211 212 def _create_service(self, data): 213 """create GenericService from json data""" 214 result = self._update_object(GenericService(), data) 215 result.name = data['display_name'] 216 service_states = {0: 'OK', 1: 'WARNING', 2: 'CRITICAL', 3: 'UNKNOWN'} 217 result.status = service_states[data['state']] 218 return result 219 220 def set_recheck(self, info_dict): 221 """schedule a forced recheck of a service or host""" 222 service = info_dict['service'] 223 host = info_dict['host'] 224 if service: 225 if self.hosts[host].services[service].is_passive_only(): 226 return 227 cmd = ['SCHEDULE_FORCED_SVC_CHECK', host, service, 'TIMESTAMP'] 228 else: 229 cmd = ['SCHEDULE_FORCED_HOST_CHECK', host, 'TIMESTAMP'] 230 self.command(';'.join(cmd)) 231 232 def set_acknowledge(self, info_dict): 233 """acknowledge a service or host""" 234 host = info_dict['host'] 235 service = info_dict['service'] 236 if service: 237 cmd = ['ACKNOWLEDGE_SVC_PROBLEM', host, service] 238 else: 239 cmd = ['ACKNOWLEDGE_HOST_PROBLEM', host] 240 cmd.extend([ 241 '2' if info_dict['sticky'] else '1', 242 '1' if info_dict['notify'] else '0', 243 '1' if info_dict['persistent'] else '0', 244 info_dict['author'], 245 info_dict['comment'], 246 ]) 247 self.command(';'.join(cmd)) 248 249 def set_downtime(self, info_dict): 250 log.info('set_downtime not implemented') 251 252 def set_submit_check_result(self, info_dict): 253 log.info('set_submit_check_result not implemented') 254 255 def get_start_end(self, host): 256 log.info('get_start_end not implemented') 257 return 'n/a', 'n/a' 258 259 def open_monitor(self, host, service=''): 260 log.info('open_monitor not implemented') 261 # TODO figure out how to add more config options like socket and weburl 262 263 def open_monitor_webpage(self): 264 log.info('open_monitor_webpage not implemented') 265 266 # TODO 267 # config dialog fields 268 # config 269