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 collections import OrderedDict 21import copy 22import datetime 23from pathlib import Path 24import platform 25import socket 26import sys 27import traceback 28import urllib.parse 29 30from bs4 import BeautifulSoup 31import requests 32 33from Nagstamon.Helpers import (host_is_filtered_out_by_re, 34 ServiceIsFilteredOutByRE, 35 StatusInformationIsFilteredOutByRE, 36 DurationIsFilteredOutByRE, 37 AttemptIsFilteredOutByRE, 38 GroupsIsFilteredOutByRE, 39 CriticalityIsFilteredOutByRE, 40 not_empty, 41 webbrowser_open, 42 STATES) 43 44from Nagstamon.Objects import (GenericService, 45 GenericHost, 46 Result) 47 48from Nagstamon.Config import (AppInfo, 49 conf, 50 debug_queue, 51 OS, 52 OS_DARWIN, 53 RESOURCES) 54 55 56# requests_gssapi is newer but not available everywhere 57try: 58 # extra imports needed to get it compiled on macOS 59 import numbers 60 import gssapi.raw.cython_converters 61 from requests_gssapi import HTTPSPNEGOAuth as HTTPSKerberos 62except ImportError: 63 from requests_kerberos import HTTPKerberosAuth as HTTPSKerberos 64 65try: 66 from requests_ecp import HTTPECPAuth 67except: 68 pass 69 70# disable annoying SubjectAltNameWarning warnings 71try: 72 from requests.packages.urllib3.exceptions import SubjectAltNameWarning 73 requests.packages.urllib3.disable_warnings(SubjectAltNameWarning) 74except: 75 # older requests version might not have the packages submodule 76 # for example the one in Ubuntu 14.04 77 pass 78 79 80class GenericServer(object): 81 82 ''' 83 Abstract server which serves as template for all other types 84 Default values are for Nagios servers 85 ''' 86 87 TYPE = 'Generic' 88 89 # dictionary to translate status bitmaps on webinterface into status flags 90 # this are defaults from Nagios 91 # 'disabled.gif' is in Nagios for hosts the same as 'passiveonly.gif' for services 92 STATUS_MAPPING = {'ack.gif': 'acknowledged', 93 'passiveonly.gif': 'passiveonly', 94 'disabled.gif': 'passiveonly', 95 'ndisabled.gif': 'notifications_disabled', 96 'downtime.gif': 'scheduled_downtime', 97 'flapping.gif': 'flapping'} 98 99 # Entries for monitor default actions in context menu 100 MENU_ACTIONS = ['Monitor', 'Recheck', 'Acknowledge', 'Submit check result', 'Downtime'] 101 102 # Arguments available for submitting check results 103 SUBMIT_CHECK_RESULT_ARGS = ['check_output', 'performance_data'] 104 105 # URLs for browser shortlinks/buttons on popup window 106 BROWSER_URLS = {'monitor': '$MONITOR$', 107 'hosts': '$MONITOR-CGI$/status.cgi?hostgroup=all&style=hostdetail&hoststatustypes=12', 108 'services': '$MONITOR-CGI$/status.cgi?host=all&servicestatustypes=253', 109 'history': '$MONITOR-CGI$/history.cgi?host=all'} 110 111 USER_AGENT = '{0}/{1}/{2}'.format(AppInfo.NAME, AppInfo.VERSION, platform.system()) 112 113 # needed to check return code of monitor server in case of false authentication 114 STATUS_CODES_NO_AUTH = [401, 403] 115 116 # default parser for BeautifulSoup - the rediscovered lxml causes trouble for Centreon so there should be choice 117 # see https://github.com/HenriWahl/Nagstamon/issues/431 118 PARSER = 'lxml' 119 120 def __init__(self, **kwds): 121 # add all keywords to object, every mode searchs inside for its favorite arguments/keywords 122 for k in kwds: 123 self.__dict__[k] = kwds[k] 124 125 self.enabled = False 126 self.type = '' 127 self.monitor_url = '' 128 self.monitor_cgi_url = '' 129 self.username = '' 130 self.password = '' 131 self.use_proxy = False 132 self.use_proxy_from_os = False 133 self.proxy_address = '' 134 self.proxy_username = '' 135 self.proxy_password = '' 136 self.auth_type = '' 137 self.encoding = None 138 self.hosts = dict() 139 self.new_hosts = dict() 140 self.isChecking = False 141 self.CheckingForNewVersion = False 142 # store current, last and difference of worst state for notification 143 self.worst_status_diff = self.worst_status_current = self.worst_status_last = 'UP' 144 self.nagitems_filtered_list = list() 145 self.nagitems_filtered = {'services': {'DISASTER': [], 'CRITICAL': [], 'HIGH': [], 146 'AVERAGE': [], 'WARNING': [], 'INFORMATION': [], 'UNKNOWN': []}, 147 'hosts': {'DOWN': [], 'UNREACHABLE': []}} 148 # number of filtered items 149 self.nagitems_filtered_count = 0 150 self.down = 0 151 self.unreachable = 0 152 self.unknown = 0 153 self.critical = 0 154 self.warning = 0 155 # zabbix support 156 self.information = 0 157 self.average = 0 158 self.high = 0 159 self.disaster = 0 160 161 self.all_ok = True 162 self.status = '' 163 self.status_description = '' 164 self.status_code = 0 165 self.has_error = False 166 self.timeout = 10 167 168 # The events_* are recycled from GUI.py 169 # history of events to track status changes for notifications 170 # events that came in 171 self.events_current = {} 172 # events that had been already displayed in popwin and need no extra mark 173 self.events_history = {} 174 # events to be given to custom notification, maybe to desktop notification too 175 self.events_notification = {} 176 177 # needed for looping server thread 178 self.thread_counter = 0 179 # needed for RecheckAll - save start_time once for not having to get it for every recheck 180 self.start_time = None 181 182 # Requests-based connections 183 self.session = None 184 185 # flag which decides if authentication has to be renewed 186 self.refresh_authentication = False 187 # flag which tells GUI if there is an TLS problem 188 self.tls_error = False 189 190 # counter for login attempts - have to be threaten differently by every monitoring server type 191 self.login_count = 0 192 193 # to handle Icinga versions this information is necessary, might be of future use for others too 194 self.version = '' 195 196 # macOS pyinstaller onefile conglomerate tends to lose cacert.pem due to macOS temp folder cleaning 197 self.cacert_path = self.cacert_content = False 198 if OS == OS_DARWIN: 199 # trying to find root path when run by pyinstaller onefile, must be something like 200 # /var/folders/7w/hfvrg7v92x3gjt95cqh974240000gn/T/_MEIQ3l3u3 201 root_path = Path(RESOURCES).parent.parent 202 if root_path.joinpath('certifi').is_dir() and root_path.joinpath('certifi', 'cacert.pem').is_file(): 203 # store path of cacert... 204 self.cacert_path = root_path.joinpath('certifi', 'cacert.pem') 205 # ...and its content 206 with open(self.cacert_path, mode='rb') as file: 207 self.cacert_content = file.read() 208 209 # Special FX 210 # Centreon 211 self.use_autologin = False 212 self.autologin_key = '' 213 # Icinga 214 self.use_display_name_host = False 215 self.use_display_name_service = False 216 # Checkmk Multisite 217 self.force_authuser = False 218 219 # OP5 api filters 220 self.host_filter = 'state !=0' 221 self.service_filter = 'state !=0 or host.state != 0' 222 223 # Sensu/Uchiwa/??? Datacenter/Site config 224 self.monitor_site = 'Site 1' 225 226 # Zabbix 227 self.use_description_name_service = None 228 229 def init_config(self): 230 ''' 231 set URLs for CGI - they are static and there is no need to set them with every cycle 232 ''' 233 # create filters like described in 234 # http://www.nagios-wiki.de/nagios/tips/host-_und_serviceproperties_fuer_status.cgi?s=servicestatustypes 235 # 236 # the following variables are not necessary anymore as with 'new' filtering 237 # 238 # hoststatus 239 # hoststatustypes = 12 240 # servicestatus 241 # servicestatustypes = 253 242 # serviceprops & hostprops both have the same values for the same states so I 243 # group them together 244 # hostserviceprops = 0 245 246 # services (unknown, warning or critical?) as dictionary, sorted by hard and soft state type 247 self.cgiurl_services = { 248 'hard': self.monitor_cgi_url + '/status.cgi?host=all&servicestatustypes=253&serviceprops=262144&limit=0', 249 'soft': self.monitor_cgi_url + '/status.cgi?host=all&servicestatustypes=253&serviceprops=524288&limit=0'} 250 # hosts (up or down or unreachable) 251 self.cgiurl_hosts = { 252 'hard': self.monitor_cgi_url + '/status.cgi?hostgroup=all&style=hostdetail&hoststatustypes=12&hostprops=262144&limit=0', 253 'soft': self.monitor_cgi_url + '/status.cgi?hostgroup=all&style=hostdetail&hoststatustypes=12&hostprops=524288&limit=0'} 254 255 def init_HTTP(self): 256 """ 257 partly not constantly working Basic Authorization requires extra Authorization headers, 258 different between various server types 259 """ 260 if self.refresh_authentication: 261 self.session = None 262 return False 263 elif self.session is None: 264 self.session = requests.Session() 265 self.session.headers['User-Agent'] = self.USER_AGENT 266 267 # support for different authentication types 268 if self.authentication == 'basic': 269 # basic authentication 270 self.session.auth = requests.auth.HTTPBasicAuth(self.username, self.password) 271 elif self.authentication == 'digest': 272 self.session.auth = requests.auth.HTTPDigestAuth(self.username, self.password) 273 elif self.authentication == 'ecp': 274 self.session.auth = HTTPECPAuth(self.idp_ecp_endpoint, username=self.username, password=self.password) 275 elif self.authentication == 'kerberos': 276 self.session.auth = HTTPSKerberos() 277 278 # default to check TLS validity 279 if self.ignore_cert: 280 self.session.verify = False 281 elif self.custom_cert_use: 282 self.session.verify = self.custom_cert_ca_file 283 else: 284 self.session.verify = True 285 286 # add proxy information 287 self.proxify(self.session) 288 return True 289 290 def proxify(self, requester): 291 ''' 292 add proxy information to session or single request 293 ''' 294 # check if proxies have to be used 295 if self.use_proxy is True: 296 if self.use_proxy_from_os is True: 297 # if .trust_enf is true the system environment will be evaluated 298 requester.trust_env = True 299 requester.proxies = dict() 300 else: 301 # check if username and password are given and provide credentials if needed 302 if self.proxy_username == self.proxy_password == '': 303 user_pass = '' 304 else: 305 user_pass = '{0}:{1}@'.format(self.proxy_username, self.proxy_password) 306 307 # split and analyze proxy URL 308 proxy_address_parts = self.proxy_address.split('//') 309 scheme = proxy_address_parts[0] 310 host_port = ''.join(proxy_address_parts[1:]) 311 312 # use only valid schemes 313 if scheme.lower() in ('http:', 'https:', 'socks5:', 'socks5h:'): 314 # merge proxy URL 315 proxy_url = '{0}//{1}{2}'.format(scheme, user_pass, host_port) 316 # fill session.proxies for both protocols 317 requester.proxies = {'http': proxy_url, 'https': proxy_url} 318 else: 319 # disable evaluation of environment variables 320 requester.trust_env = False 321 requester.proxies = None 322 323 def reset_HTTP(self): 324 ''' 325 if authentication fails try to reset any HTTP session stuff - might be different for different monitors 326 ''' 327 self.session = None 328 329 def get_name(self): 330 ''' 331 return stringified name 332 ''' 333 return str(self.name) 334 335 def get_username(self): 336 ''' 337 return stringified username 338 ''' 339 return str(self.username) 340 341 def get_password(self): 342 ''' 343 return stringified password 344 ''' 345 return str(self.password) 346 347 def get_server_version(self): 348 ''' 349 dummy function, at the moment only used by Icinga 350 ''' 351 pass 352 353 def set_recheck(self, info_dict): 354 self._set_recheck(info_dict['host'], info_dict['service']) 355 356 def _set_recheck(self, host, service): 357 if service != '': 358 if self.hosts[host].services[service].is_passive_only(): 359 # Do not check passive only checks 360 return 361 try: 362 # get start time from Nagios as HTML to use same timezone setting like the locally installed Nagios 363 result = self.FetchURL( 364 self.monitor_cgi_url + '/cmd.cgi?' + urllib.parse.urlencode({'cmd_typ': '96', 'host': host})) 365 self.start_time = dict(result.result.find(attrs={'name': 'start_time'}).attrs)['value'] 366 # decision about host or service - they have different URLs 367 if service == '': 368 # host 369 cmd_typ = '96' 370 else: 371 # service @ host 372 cmd_typ = '7' 373 # ignore empty service in case of rechecking a host 374 cgi_data = urllib.parse.urlencode([('cmd_typ', cmd_typ), 375 ('cmd_mod', '2'), 376 ('host', host), 377 ('service', service), 378 ('start_time', self.start_time), 379 ('force_check', 'on'), 380 ('btnSubmit', 'Commit')]) 381 # execute POST request 382 self.FetchURL(self.monitor_cgi_url + '/cmd.cgi', giveback='raw', cgi_data=cgi_data) 383 except: 384 traceback.print_exc(file=sys.stdout) 385 386 def set_acknowledge(self, info_dict): 387 ''' 388 different monitors might have different implementations of _set_acknowledge 389 ''' 390 if info_dict['acknowledge_all_services'] is True: 391 all_services = info_dict['all_services'] 392 else: 393 all_services = [] 394 self._set_acknowledge(info_dict['host'], 395 info_dict['service'], 396 info_dict['author'], 397 info_dict['comment'], 398 info_dict['sticky'], 399 info_dict['notify'], 400 info_dict['persistent'], 401 all_services) 402 403 404 def _set_acknowledge(self, host, service, author, comment, sticky, notify, persistent, all_services=[]): 405 ''' 406 send acknowledge to monitor server - might be different on every monitor type 407 ''' 408 409 url = self.monitor_cgi_url + '/cmd.cgi' 410 411 # the following flags apply to hosts and services 412 # 413 # according to sf.net bug #3304098 (https://sourceforge.net/tracker/?func=detail&atid=1101370&aid=3304098&group_id=236865) 414 # the send_notification-flag must not exist if it is set to 'off', otherwise 415 # the Nagios core interpretes it as set, regardless its real value 416 # 417 # for whatever silly reason Icinga depends on the correct order of submitted form items... 418 # see sf.net bug 3428844 419 # 420 # Thanks to Icinga ORDER OF ARGUMENTS IS IMPORTANT HERE! 421 # 422 cgi_data = OrderedDict() 423 if service == '': 424 cgi_data['cmd_typ'] = '33' 425 else: 426 cgi_data['cmd_typ'] = '34' 427 cgi_data['cmd_mod'] = '2' 428 cgi_data['host'] = host 429 if service != '': 430 cgi_data['service'] = service 431 cgi_data['com_author'] = author 432 cgi_data['com_data'] = comment 433 cgi_data['btnSubmit'] = 'Commit' 434 if notify is True: 435 cgi_data['send_notification'] = 'on' 436 if persistent is True: 437 cgi_data['persistent'] = 'on' 438 if sticky is True: 439 cgi_data['sticky_ack'] = 'on' 440 441 self.FetchURL(url, giveback='raw', cgi_data=cgi_data) 442 443 # acknowledge all services on a host 444 if len(all_services) > 0: 445 for s in all_services: 446 cgi_data['cmd_typ'] = '34' 447 cgi_data['service'] = s 448 self.FetchURL(url, giveback='raw', cgi_data=cgi_data) 449 450 def set_downtime(self, info_dict): 451 ''' 452 different monitors might have different implementations of _set_downtime 453 ''' 454 self._set_downtime(info_dict['host'], 455 info_dict['service'], 456 info_dict['author'], 457 info_dict['comment'], 458 info_dict['fixed'], 459 info_dict['start_time'], 460 info_dict['end_time'], 461 info_dict['hours'], 462 info_dict['minutes']) 463 464 def _set_downtime(self, host, service, author, comment, fixed, start_time, end_time, hours, minutes): 465 ''' 466 finally send downtime command to monitor server 467 ''' 468 url = self.monitor_cgi_url + '/cmd.cgi' 469 470 # for some reason Icinga is very fastidiuos about the order of CGI arguments, so please 471 # here we go... it took DAYS :-( 472 cgi_data = OrderedDict() 473 if service == '': 474 cgi_data['cmd_typ'] = '55' 475 else: 476 cgi_data['cmd_typ'] = '56' 477 cgi_data['cmd_mod'] = '2' 478 cgi_data['trigger'] = '0' 479 cgi_data['host'] = host 480 if service != '': 481 cgi_data['service'] = service 482 cgi_data['com_author'] = author 483 cgi_data['com_data'] = comment 484 cgi_data['fixed'] = fixed 485 cgi_data['start_time'] = start_time 486 cgi_data['end_time'] = end_time 487 cgi_data['hours'] = hours 488 cgi_data['minutes'] = minutes 489 cgi_data['btnSubmit'] = 'Commit' 490 491 # running remote cgi command 492 self.FetchURL(url, giveback='raw', cgi_data=cgi_data) 493 494 def set_submit_check_result(self, info_dict): 495 """ 496 start specific submission part 497 """ 498 self._set_submit_check_result(info_dict['host'], 499 info_dict['service'], 500 info_dict['state'], 501 info_dict['comment'], 502 info_dict['check_output'], 503 info_dict['performance_data']) 504 505 def _set_submit_check_result(self, host, service, state, comment, check_output, performance_data): 506 ''' 507 worker for submitting check result 508 ''' 509 url = self.monitor_cgi_url + '/cmd.cgi' 510 511 # decision about host or service - they have different URLs 512 if service == '': 513 # host 514 cgi_data = urllib.parse.urlencode([('cmd_typ', '87'), ('cmd_mod', '2'), ('host', host), 515 ('plugin_state', {'up': '0', 'down': '1', 'unreachable': '2'}[state]), 516 ('plugin_output', check_output), 517 ('performance_data', performance_data), ('btnSubmit', 'Commit')]) 518 self.FetchURL(url, giveback='raw', cgi_data=cgi_data) 519 520 if service != '': 521 # service @ host 522 cgi_data = urllib.parse.urlencode( 523 [('cmd_typ', '30'), ('cmd_mod', '2'), ('host', host), ('service', service), 524 ('plugin_state', {'ok': '0', 'warning': '1', 'critical': '2', 'unknown': '3'}[state]), 525 ('plugin_output', check_output), 526 ('performance_data', performance_data), ('btnSubmit', 'Commit')]) 527 # running remote cgi command 528 self.FetchURL(url, giveback='raw', cgi_data=cgi_data) 529 530 def get_start_end(self, host): 531 ''' 532 for GUI to get actual downtime start and end from server - they may vary so it's better to get 533 directly from web interface 534 ''' 535 try: 536 result = self.FetchURL( 537 self.monitor_cgi_url + '/cmd.cgi?' + urllib.parse.urlencode({'cmd_typ': '55', 'host': host})) 538 start_time = dict(result.result.find(attrs={'name': 'start_time'}).attrs)['value'] 539 end_time = dict(result.result.find(attrs={'name': 'end_time'}).attrs)['value'] 540 # give values back as tuple 541 return start_time, end_time 542 except Exception: 543 self.Error(sys.exc_info()) 544 return 'n/a', 'n/a' 545 546 def open_monitor(self, host, service=''): 547 ''' 548 open monitor from tablewidget context menu 549 ''' 550 # only type is important so do not care of service '' in case of host monitor 551 if service == '': 552 typ = 1 553 else: 554 typ = 2 555 if conf.debug_mode: 556 self.Debug(server=self.get_name(), host=host, service=service, 557 debug='Open host/service monitor web page ' + self.monitor_cgi_url + '/extinfo.cgi?' + urllib.parse.urlencode( 558 {'type': typ, 'host': host, 'service': service})) 559 webbrowser_open(self.monitor_cgi_url + '/extinfo.cgi?' + urllib.parse.urlencode( 560 {'type': typ, 'host': host, 'service': service})) 561 562 def open_monitor_webpage(self): 563 ''' 564 open monitor from systray/toparea context menu 565 ''' 566 567 if conf.debug_mode: 568 self.Debug(server=self.get_name(), 569 debug='Open monitor web page ' + self.monitor_cgi_url) 570 webbrowser_open(self.monitor_url) 571 572 def _get_status(self): 573 ''' 574 Get status from Nagios Server 575 ''' 576 # create Nagios items dictionary with to lists for services and hosts 577 # every list will contain a dictionary for every failed service/host 578 # this dictionary is only temporarily 579 nagitems = {'services': [], 'hosts': []} 580 581 # new_hosts dictionary 582 self.new_hosts = dict() 583 584 # hosts - mostly the down ones 585 # unfortunately the hosts status page has a different structure so 586 # hosts must be analyzed separately 587 try: 588 for status_type in 'hard', 'soft': 589 result = self.FetchURL(self.cgiurl_hosts[status_type]) 590 htobj, error, status_code = result.result, result.error, result.status_code 591 592 # check if any error occured 593 errors_occured = self.check_for_error(htobj, error, status_code) 594 # if there are errors return them 595 if errors_occured is not False: 596 return(errors_occured) 597 598 # put a copy of a part of htobj into table to be able to delete htobj 599 # too mnuch copy.deepcopy()s here give recursion crashs 600 table = htobj('table', {'class': 'status'})[0] 601 602 # access table rows 603 # some Icinga versions have a <tbody> tag in cgi output HTML which 604 # omits the <tr> tags being found 605 if len(table('tbody')) == 0: 606 trs = table('tr', recursive=False) 607 else: 608 tbody = table('tbody')[0] 609 trs = tbody('tr', recursive=False) 610 611 # do some cleanup 612 del result, error 613 614 # kick out table heads 615 trs.pop(0) 616 617 # dummy tds to be deleteable 618 tds = [] 619 620 for tr in trs: 621 try: 622 # ignore empty <tr> rows 623 if len(tr('td', recursive=False)) > 1: 624 n = dict() 625 # get tds in one tr 626 tds = tr('td', recursive=False) 627 # host 628 try: 629 n['host'] = str(tds[0].table.tr.td.table.tr.td.a.text) 630 except Exception: 631 n['host'] = str(nagitems[len(nagitems) - 1]['host']) 632 # status 633 n['status'] = str(tds[1].text) 634 # last_check 635 n['last_check'] = str(tds[2].text) 636 # duration 637 n['duration'] = str(tds[3].text) 638 # division between Nagios and Icinga in real life... where 639 # Nagios has only 5 columns there are 7 in Icinga 1.3... 640 # ... and 6 in Icinga 1.2 :-) 641 if len(tds) < 7: 642 # the old Nagios table 643 # status_information 644 if len(tds[4](text=not_empty)) == 0: 645 n['status_information'] = '' 646 else: 647 n['status_information'] = str(tds[4].text).replace('\n', ' ').replace('\t', ' ').strip() 648 # attempts are not shown in case of hosts so it defaults to 'n/a' 649 n['attempt'] = 'n/a' 650 else: 651 # attempts are shown for hosts 652 # to fix http://sourceforge.net/tracker/?func=detail&atid=1101370&aid=3280961&group_id=236865 .attempt needs 653 # to be stripped 654 n['attempt'] = str(tds[4].text).strip() 655 # status_information 656 if len(tds[5](text=not_empty)) == 0: 657 n['status_information'] = '' 658 else: 659 n['status_information'] = str(tds[5].text).replace('\n', ' ').replace('\t', ' ').strip() 660 # status flags 661 n['passiveonly'] = False 662 n['notifications_disabled'] = False 663 n['flapping'] = False 664 n['acknowledged'] = False 665 n['scheduled_downtime'] = False 666 667 # map status icons to status flags 668 icons = tds[0].findAll('img') 669 for i in icons: 670 icon = i['src'].split('/')[-1] 671 if icon in self.STATUS_MAPPING: 672 n[self.STATUS_MAPPING[icon]] = True 673 # cleaning 674 del icons 675 676 # add dictionary full of information about this host item to nagitems 677 nagitems['hosts'].append(n) 678 # after collection data in nagitems create objects from its informations 679 # host objects contain service objects 680 if n['host'] not in self.new_hosts: 681 new_host = n['host'] 682 self.new_hosts[new_host] = GenericHost() 683 self.new_hosts[new_host].name = n['host'] 684 self.new_hosts[new_host].server = self.name 685 self.new_hosts[new_host].status = n['status'] 686 self.new_hosts[new_host].last_check = n['last_check'] 687 self.new_hosts[new_host].duration = n['duration'] 688 self.new_hosts[new_host].attempt = n['attempt'] 689 # ##self.new_hosts[new_host].status_information = n['status_information'].encode('utf-8') 690 self.new_hosts[new_host].status_information = n['status_information'] 691 self.new_hosts[new_host].passiveonly = n['passiveonly'] 692 self.new_hosts[new_host].notifications_disabled = n['notifications_disabled'] 693 self.new_hosts[new_host].flapping = n['flapping'] 694 self.new_hosts[new_host].acknowledged = n['acknowledged'] 695 self.new_hosts[new_host].scheduled_downtime = n['scheduled_downtime'] 696 self.new_hosts[new_host].status_type = status_type 697 del tds, n 698 except Exception: 699 self.Error(sys.exc_info()) 700 701 # do some cleanup 702 htobj.decompose() 703 del htobj, trs, table 704 705 except Exception: 706 # set checking flag back to False 707 self.isChecking = False 708 result, error = self.Error(sys.exc_info()) 709 return Result(result=result, error=error) 710 711 # services 712 try: 713 for status_type in 'hard', 'soft': 714 result = self.FetchURL(self.cgiurl_services[status_type]) 715 htobj, error, status_code = result.result, result.error, result.status_code 716 717 # check if any error occured 718 errors_occured = self.check_for_error(htobj, error, status_code) 719 # if there are errors return them 720 if errors_occured is not False: 721 return(errors_occured) 722 723 # too much copy.deepcopy()s here give recursion crashs 724 table = htobj('table', {'class': 'status'})[0] 725 726 # some Icinga versions have a <tbody> tag in cgi output HTML which 727 # omits the <tr> tags being found 728 if len(table('tbody')) == 0: 729 trs = table('tr', recursive=False) 730 else: 731 tbody = table('tbody')[0] 732 trs = tbody('tr', recursive=False) 733 734 del result, error 735 736 # kick out table heads 737 trs.pop(0) 738 739 # dummy tds to be deleteable 740 tds = [] 741 742 for tr in trs: 743 try: 744 # ignore empty <tr> rows - there are a lot of them - a Nagios bug? 745 tds = tr('td', recursive=False) 746 if len(tds) > 1: 747 n = dict() 748 # host 749 # the resulting table of Nagios status.cgi table omits the 750 # hostname of a failing service if there are more than one 751 # so if the hostname is empty the nagios status item should get 752 # its hostname from the previuos item - one reason to keep 'nagitems' 753 try: 754 n['host'] = str(tds[0](text=not_empty)[0]) 755 except Exception: 756 n['host'] = str(nagitems['services'][len(nagitems['services']) - 1]['host']) 757 # service 758 n['service'] = str(tds[1](text=not_empty)[0]) 759 # status 760 n['status'] = str(tds[2](text=not_empty)[0]) 761 # last_check 762 n['last_check'] = str(tds[3](text=not_empty)[0]) 763 # duration 764 n['duration'] = str(tds[4](text=not_empty)[0]) 765 # attempt 766 # to fix http://sourceforge.net/tracker/?func=detail&atid=1101370&aid=3280961&group_id=236865 .attempt needs 767 # to be stripped 768 n['attempt'] = str(tds[5](text=not_empty)[0]).strip() 769 # status_information 770 if len(tds[6](text=not_empty)) == 0: 771 n['status_information'] = '' 772 else: 773 n['status_information'] = str(tds[6].text).replace('\n', ' ').replace('\t', ' ').strip() 774 # status flags 775 n['passiveonly'] = False 776 n['notifications_disabled'] = False 777 n['flapping'] = False 778 n['acknowledged'] = False 779 n['scheduled_downtime'] = False 780 781 # map status icons to status flags 782 icons = tds[1].findAll('img') 783 for i in icons: 784 icon = i['src'].split('/')[-1] 785 if icon in self.STATUS_MAPPING: 786 n[self.STATUS_MAPPING[icon]] = True 787 # cleaning 788 del icons 789 790 # add dictionary full of information about this service item to nagitems - only if service 791 nagitems['services'].append(n) 792 # after collection data in nagitems create objects of its informations 793 # host objects contain service objects 794 if n['host'] not in self.new_hosts: 795 self.new_hosts[n['host']] = GenericHost() 796 self.new_hosts[n['host']].name = n['host'] 797 self.new_hosts[n['host']].status = 'UP' 798 # trying to fix https://sourceforge.net/tracker/index.php?func=detail&aid=3299790&group_id=236865&atid=1101370 799 # if host is not down but in downtime or any other flag this should be evaluated too 800 # map status icons to status flags 801 icons = tds[0].findAll('img') 802 for i in icons: 803 icon = i['src'].split('/')[-1] 804 if icon in self.STATUS_MAPPING: 805 self.new_hosts[n['host']].__dict__[self.STATUS_MAPPING[icon]] = True 806 807 # if a service does not exist create its object 808 if n['service'] not in self.new_hosts[n['host']].services: 809 new_service = n['service'] 810 self.new_hosts[n['host']].services[new_service] = GenericService() 811 self.new_hosts[n['host']].services[new_service].host = n['host'] 812 self.new_hosts[n['host']].services[new_service].name = n['service'] 813 self.new_hosts[n['host']].services[new_service].server = self.name 814 self.new_hosts[n['host']].services[new_service].status = n['status'] 815 self.new_hosts[n['host']].services[new_service].last_check = n['last_check'] 816 self.new_hosts[n['host']].services[new_service].duration = n['duration'] 817 self.new_hosts[n['host']].services[new_service].attempt = n['attempt'] 818 self.new_hosts[n['host']].services[new_service].status_information = n['status_information'] 819 self.new_hosts[n['host']].services[new_service].passiveonly = n['passiveonly'] 820 self.new_hosts[n['host']].services[new_service].notifications_disabled = n[ 821 'notifications_disabled'] 822 self.new_hosts[n['host']].services[new_service].flapping = n['flapping'] 823 self.new_hosts[n['host']].services[new_service].acknowledged = n['acknowledged'] 824 self.new_hosts[n['host']].services[new_service].scheduled_downtime = n['scheduled_downtime'] 825 self.new_hosts[n['host']].services[new_service].status_type = status_type 826 del tds, n 827 except Exception: 828 self.Error(sys.exc_info()) 829 830 # do some cleanup 831 htobj.decompose() 832 del htobj, trs, table 833 834 except Exception: 835 # set checking flag back to False 836 self.isChecking = False 837 result, error = self.Error(sys.exc_info()) 838 return Result(result=result, error=error) 839 840 # some cleanup 841 del(nagitems) 842 843 # dummy return in case all is OK 844 return Result() 845 846 def GetStatus(self, output=None): 847 ''' 848 get nagios status information from cgiurl and give it back 849 as dictionary 850 output parameter is needed in case authentication failed so that popwin might ask for credentials 851 ''' 852 853 # set checking flag to be sure only one thread cares about this server 854 self.isChecking = True 855 856 # check if server is enabled, if not, do not get any status 857 if self.enabled is False: 858 self.worst_status_diff = 'UP' 859 self.isChecking = False 860 return Result() 861 862 # initialize HTTP first 863 self.init_HTTP() 864 865 # get all trouble hosts/services from server specific _get_status() 866 status = self._get_status() 867 868 if status is not None: 869 self.status = status.result 870 self.status_description = status.error 871 self.status_code = status.status_code 872 else: 873 return Result() 874 875 # some monitor server seem to have a problem with too short intervals 876 # and sometimes send a bad status line which would result in a misleading 877 # ERROR display - it seems safe to ignore these errors 878 # see https://github.com/HenriWahl/Nagstamon/issues/207 879 # Update: Another strange error to ignore is ConnectionResetError 880 # see https://github.com/HenriWahl/Nagstamon/issues/295 881 if 'BadStatusLine' in self.status_description or\ 882 'ConnectionResetError' in self.status_description: 883 self.status_description = '' 884 self.isChecking = False 885 return Result(result=self.status, 886 error=self.status_description, 887 status_code=self.status_code) 888 889 if (self.status == 'ERROR' or 890 self.status_description != '' or 891 self.status_code >= 400): 892 893 # ask for password if authorization failed 894 if 'HTTP Error 401' in self.status_description or \ 895 'HTTP Error 403' in self.status_description or \ 896 'HTTP Error 500' in self.status_description or \ 897 'bad session id' in self.status_description.lower() or \ 898 'login failed' in self.status_description.lower() or \ 899 self.status_code in self.STATUS_CODES_NO_AUTH: 900 if conf.servers[self.name].enabled is True: 901 # needed to get valid credentials 902 self.refresh_authentication = True 903 # clean existent authentication 904 self.reset_HTTP() 905 self.init_HTTP() 906 status = self._get_status() 907 self.status = status.result 908 self.status_description = status.error 909 self.status_code = status.status_code 910 return(status) 911 elif self.status_description.startswith('requests.exceptions.SSLError:'): 912 self.tls_error = True 913 else: 914 self.isChecking = False 915 self.tls_error = False 916 return Result(result=self.status, 917 error=self.status_description, 918 status_code=self.status_code) 919 920 # no new authentication needed 921 self.refresh_authentication = False 922 923 # this part has been before in GUI.RefreshDisplay() - wrong place, here it needs to be reset 924 self.nagitems_filtered = {'services': {'DISASTER': [], 'CRITICAL': [], 'HIGH': [], 925 'AVERAGE': [], 'WARNING': [], 'INFORMATION': [], 'UNKNOWN': []}, 926 'hosts': {'DOWN': [], 'UNREACHABLE': []}} 927 928 # initialize counts for various service/hosts states 929 # count them with every miserable host/service respective to their meaning 930 self.down = 0 931 self.unreachable = 0 932 self.unknown = 0 933 self.critical = 0 934 self.warning = 0 935 # zabbix support 936 self.information = 0 937 self.average = 0 938 self.high = 0 939 self.disaster = 0 940 941 for host in self.new_hosts.values(): 942 # Don't enter the loop if we don't have a problem. Jump down to your problem services 943 if not host.status == 'UP': 944 # add hostname for sorting 945 host.host = host.name 946 # Some generic filters 947 if host.acknowledged is True and conf.filter_acknowledged_hosts_services is True: 948 if conf.debug_mode: 949 self.Debug(server=self.get_name(), debug='Filter: ACKNOWLEDGED ' + str(host.name)) 950 host.visible = False 951 952 if host.notifications_disabled is True and\ 953 conf.filter_hosts_services_disabled_notifications is True: 954 if conf.debug_mode: 955 self.Debug(server=self.get_name(), debug='Filter: NOTIFICATIONS ' + str(host.name)) 956 host.visible = False 957 958 if host.passiveonly is True and conf.filter_hosts_services_disabled_checks is True: 959 if conf.debug_mode: 960 self.Debug(server=self.get_name(), debug='Filter: PASSIVEONLY ' + str(host.name)) 961 host.visible = False 962 963 if host.scheduled_downtime is True and conf.filter_hosts_services_maintenance is True: 964 if conf.debug_mode: 965 self.Debug(server=self.get_name(), debug='Filter: DOWNTIME ' + str(host.name)) 966 host.visible = False 967 968 if host.flapping is True and conf.filter_all_flapping_hosts is True: 969 if conf.debug_mode: 970 self.Debug(server=self.get_name(), debug='Filter: FLAPPING HOST ' + str(host.name)) 971 host.visible = False 972 973 # Checkmk and OP5 do not show the status_type so their host.status_type will be empty 974 if host.status_type != '': 975 if conf.filter_hosts_in_soft_state is True and host.status_type == 'soft': 976 if conf.debug_mode: 977 self.Debug(server=self.get_name(), debug='Filter: SOFT STATE ' + str(host.name)) 978 host.visible = False 979 980 if host_is_filtered_out_by_re(host.name, conf) is True: 981 if conf.debug_mode: 982 self.Debug(server=self.get_name(), debug='Filter: REGEXP ' + str(host.name)) 983 host.visible = False 984 985 if StatusInformationIsFilteredOutByRE(host.status_information, conf) is True: 986 if conf.debug_mode: 987 self.Debug(server=self.get_name(), debug='Filter: REGEXP ' + str(host.name)) 988 host.visible = False 989 990 # The Criticality filter can be used only with centreon objects. Other objects don't have the criticality attribute. 991 if self.type == 'Centreon': 992 if CriticalityIsFilteredOutByRE(host.criticality, conf): 993 if conf.debug_mode: 994 self.Debug(server=self.get_name(), debug='Filter: REGEXP Criticality ' + str(host.name)) 995 host.visible = False 996 997 # Finegrain for the specific state 998 if host.status == 'DOWN': 999 if conf.filter_all_down_hosts is True: 1000 if conf.debug_mode: 1001 self.Debug(server=self.get_name(), debug='Filter: DOWN ' + str(host.name)) 1002 host.visible = False 1003 1004 if host.visible: 1005 self.nagitems_filtered['hosts']['DOWN'].append(host) 1006 self.down += 1 1007 1008 if host.status == 'UNREACHABLE': 1009 if conf.filter_all_unreachable_hosts is True: 1010 if conf.debug_mode: 1011 self.Debug(server=self.get_name(), debug='Filter: UNREACHABLE ' + str(host.name)) 1012 host.visible = False 1013 1014 if host.visible: 1015 self.nagitems_filtered['hosts']['UNREACHABLE'].append(host) 1016 self.unreachable += 1 1017 1018 # Add host flags for status icons in treeview 1019 if host.acknowledged: 1020 host.host_flags += 'A' 1021 if host.scheduled_downtime: 1022 host.host_flags += 'D' 1023 if host.flapping: 1024 host.host_flags += 'F' 1025 if host.passiveonly: 1026 host.host_flags += 'P' 1027 1028 for service in host.services.values(): 1029 # add service name for sorting 1030 service.service = service.name 1031 # Some generic filtering 1032 if service.acknowledged is True and conf.filter_acknowledged_hosts_services is True: 1033 if conf.debug_mode: 1034 self.Debug(server=self.get_name(), 1035 debug='Filter: ACKNOWLEDGED ' + str(host.name) + ';' + str(service.name)) 1036 service.visible = False 1037 1038 if service.notifications_disabled is True and\ 1039 conf.filter_hosts_services_disabled_notifications is True: 1040 if conf.debug_mode: 1041 self.Debug(server=self.get_name(), 1042 debug='Filter: NOTIFICATIONS ' + str(host.name) + ';' + str(service.name)) 1043 1044 service.visible = False 1045 1046 if service.passiveonly is True and conf.filter_hosts_services_disabled_checks is True: 1047 if conf.debug_mode: 1048 self.Debug(server=self.get_name(), 1049 debug='Filter: PASSIVEONLY ' + str(host.name) + ';' + str(service.name)) 1050 service.visible = False 1051 1052 if service.scheduled_downtime is True and conf.filter_hosts_services_maintenance is True: 1053 if conf.debug_mode: 1054 self.Debug(server=self.get_name(), 1055 debug='Filter: DOWNTIME ' + str(host.name) + ';' + str(service.name)) 1056 service.visible = False 1057 1058 if service.flapping is True and conf.filter_all_flapping_services is True: 1059 if conf.debug_mode: 1060 self.Debug(server=self.get_name(), 1061 debug='Filter: FLAPPING SERVICE ' + str(host.name) + ';' + str(service.name)) 1062 service.visible = False 1063 1064 if host.scheduled_downtime is True and conf.filter_services_on_hosts_in_maintenance is True: 1065 if conf.debug_mode: 1066 self.Debug(server=self.get_name(), 1067 debug='Filter: Service on host in DOWNTIME ' + str(host.name) + ';' + str( 1068 service.name)) 1069 service.visible = False 1070 1071 if host.acknowledged is True and conf.filter_services_on_acknowledged_hosts is True: 1072 if conf.debug_mode: 1073 self.Debug(server=self.get_name(), 1074 debug='Filter: Service on acknowledged host' + str(host.name) + ';' + str( 1075 service.name)) 1076 service.visible = False 1077 1078 if host.status == 'DOWN' and conf.filter_services_on_down_hosts is True: 1079 if conf.debug_mode: 1080 self.Debug(server=self.get_name(), 1081 debug='Filter: Service on host in DOWN ' + str(host.name) + ';' + str(service.name)) 1082 service.visible = False 1083 1084 if host.status == 'UNREACHABLE' and conf.filter_services_on_unreachable_hosts is True: 1085 if conf.debug_mode: 1086 self.Debug(server=self.get_name(), 1087 debug='Filter: Service on host in UNREACHABLE ' + str(host.name) + ';' + str( 1088 service.name)) 1089 service.visible = False 1090 1091 if conf.filter_all_unreachable_services is True and service.unreachable is True: 1092 if conf.debug_mode: 1093 self.Debug(server=self.get_name(), debug='Filter: UNREACHABLE ' + str(host.name) + ';' + str( 1094 service.name)) 1095 service.visible = False 1096 1097 # Checkmk and OP5 do not show the status_type so their host.status_type will be empty 1098 if service.status_type != '': 1099 if conf.filter_services_in_soft_state is True and service.status_type == 'soft': 1100 if conf.debug_mode: 1101 self.Debug(server=self.get_name(), 1102 debug='Filter: SOFT STATE ' + str(host.name) + ';' + str(service.name)) 1103 service.visible = False 1104 # fix for https://github.com/HenriWahl/Nagstamon/issues/654 1105 elif not self.TYPE.startswith("Zabbix"): 1106 if len(service.attempt) < 3: 1107 service.visible = True 1108 elif len(service.attempt) == 3: 1109 # fixing a bug introduced in 038fa34 for zabbix service name in attempt 1110 if service.attempt.find("/") == -1: 1111 service.visible = True 1112 else: 1113 # the old, actually wrong, behaviour 1114 real_attempt, max_attempt = service.attempt.split('/') 1115 if real_attempt != max_attempt and conf.filter_services_in_soft_state is True: 1116 if conf.debug_mode: 1117 self.Debug(server=self.get_name(), 1118 debug='Filter: SOFT STATE ' + str(host.name) + ';' + str(service.name)) 1119 service.visible = False 1120 1121 if host_is_filtered_out_by_re(host.name, conf) is True: 1122 if conf.debug_mode: 1123 self.Debug(server=self.get_name(), 1124 debug='Filter: REGEXP ' + str(host.name) + ';' + str(service.name)) 1125 service.visible = False 1126 1127 if ServiceIsFilteredOutByRE(service.get_name(), conf) is True: 1128 if conf.debug_mode: 1129 self.Debug(server=self.get_name(), 1130 debug='Filter: REGEXP ' + str(host.name) + ';' + str(service.name)) 1131 service.visible = False 1132 1133 if StatusInformationIsFilteredOutByRE(service.status_information, conf) is True: 1134 if conf.debug_mode: 1135 self.Debug(server=self.get_name(), 1136 debug='Filter: REGEXP ' + str(host.name) + ';' + str(service.name)) 1137 service.visible = False 1138 1139 if DurationIsFilteredOutByRE(service.duration, conf) is True: 1140 if conf.debug_mode: 1141 self.Debug(server=self.get_name(), 1142 debug='Filter: REGEXP ' + str(host.name) + ';' + str(service.name)) 1143 service.visible = False 1144 1145 if AttemptIsFilteredOutByRE(service.attempt, conf) is True: 1146 if conf.debug_mode: 1147 self.Debug(server=self.get_name(), 1148 debug='Filter: REGEXP ' + str(host.name) + ';' + str(service.name)) 1149 service.visible = False 1150 1151 if GroupsIsFilteredOutByRE(service.groups, conf) is True: 1152 if conf.debug_mode: 1153 self.Debug(server=self.get_name(), 1154 debug='Filter: REGEXP ' + str(host.name) + ';' + str(service.name)) 1155 service.visible = False 1156 1157 # The Criticality filter can be used only with centreon objects. Other objects don't have the criticality attribute. 1158 if self.type == 'Centreon': 1159 if CriticalityIsFilteredOutByRE(service.criticality, conf): 1160 if conf.debug_mode: 1161 self.Debug(server=self.get_name(), debug='Filter: REGEXP Criticality %s;%s %s' % ( 1162 (str(host.name), str(service.name), str(service.criticality)))) 1163 service.visible = False 1164 1165 # Finegrain for the specific state 1166 if service.visible: 1167 if service.status == 'DISASTER': 1168 if conf.filter_all_disaster_services is True: 1169 if conf.debug_mode: 1170 self.Debug(server=self.get_name(), 1171 debug='Filter: DISASTER ' + str(host.name) + ';' + str(service.name)) 1172 service.visible = False 1173 else: 1174 self.nagitems_filtered['services']['DISASTER'].append(service) 1175 self.disaster += 1 1176 1177 if service.status == 'CRITICAL': 1178 if conf.filter_all_critical_services is True: 1179 if conf.debug_mode: 1180 self.Debug(server=self.get_name(), 1181 debug='Filter: CRITICAL ' + str(host.name) + ';' + str(service.name)) 1182 service.visible = False 1183 else: 1184 self.nagitems_filtered['services']['CRITICAL'].append(service) 1185 self.critical += 1 1186 1187 if service.status == 'HIGH': 1188 if conf.filter_all_high_services is True: 1189 if conf.debug_mode: 1190 self.Debug(server=self.get_name(), 1191 debug='Filter: HIGH ' + str(host.name) + ';' + str(service.name)) 1192 service.visible = False 1193 else: 1194 self.nagitems_filtered['services']['HIGH'].append(service) 1195 self.high += 1 1196 1197 if service.status == 'AVERAGE': 1198 if conf.filter_all_average_services is True: 1199 if conf.debug_mode: 1200 self.Debug(server=self.get_name(), 1201 debug='Filter: AVERAGE ' + str(host.name) + ';' + str(service.name)) 1202 service.visible = False 1203 else: 1204 self.nagitems_filtered['services']['AVERAGE'].append(service) 1205 self.average += 1 1206 1207 if service.status == 'WARNING': 1208 if conf.filter_all_warning_services is True: 1209 if conf.debug_mode: 1210 self.Debug(server=self.get_name(), 1211 debug='Filter: WARNING ' + str(host.name) + ';' + str(service.name)) 1212 service.visible = False 1213 else: 1214 self.nagitems_filtered['services']['WARNING'].append(service) 1215 self.warning += 1 1216 1217 if service.status == 'INFORMATION': 1218 if conf.filter_all_information_services is True: 1219 if conf.debug_mode: 1220 self.Debug(server=self.get_name(), 1221 debug='Filter: INFORMATION ' + str(host.name) + ';' + str(service.name)) 1222 service.visible = False 1223 else: 1224 self.nagitems_filtered['services']['INFORMATION'].append(service) 1225 self.information += 1 1226 1227 if service.status == 'UNKNOWN': 1228 if conf.filter_all_unknown_services is True: 1229 if conf.debug_mode: 1230 self.Debug(server=self.get_name(), 1231 debug='Filter: UNKNOWN ' + str(host.name) + ';' + str(service.name)) 1232 service.visible = False 1233 else: 1234 self.nagitems_filtered['services']['UNKNOWN'].append(service) 1235 self.unknown += 1 1236 1237 # zabbix support 1238 if service.status == "INFORMATION": 1239 if conf.filter_all_unknown_services is True: 1240 if conf.debug_mode: 1241 self.Debug(server=self.get_name(), 1242 debug="Filter: INFORMATION " + str(host.name) + ";" + str(service.name)) 1243 service.visible = False 1244 else: 1245 self.nagitems_filtered["services"]["INFORMATION"].append(service) 1246 self.information += 1 1247 1248 if service.status == "AVERAGE": 1249 if conf.filter_all_unknown_services is True: 1250 if conf.debug_mode: 1251 self.Debug(server=self.get_name(), 1252 debug="Filter: AVERAGE " + str(host.name) + ";" + str(service.name)) 1253 service.visible = False 1254 else: 1255 self.nagitems_filtered["services"]["AVERAGE"].append(service) 1256 self.average += 1 1257 1258 if service.status == "HIGH": 1259 if conf.filter_all_unknown_services is True: 1260 if conf.debug_mode: 1261 self.Debug(server=self.get_name(), 1262 debug="Filter: HIGH " + str(host.name) + ";" + str(service.name)) 1263 service.visible = False 1264 else: 1265 self.nagitems_filtered["services"]["HIGH"].append(service) 1266 self.high += 1 1267 1268 if service.status == "DISASTER": 1269 if conf.filter_all_unknown_services is True: 1270 if conf.debug_mode: 1271 self.Debug(server=self.get_name(), 1272 debug="Filter: DISASTER " + str(host.name) + ";" + str(service.name)) 1273 service.visible = False 1274 else: 1275 self.nagitems_filtered["services"]["DISASTER"].append(service) 1276 self.disaster += 1 1277 1278 # Add service flags for status icons in treeview 1279 if service.acknowledged: 1280 service.service_flags += 'A' 1281 if service.scheduled_downtime: 1282 service.service_flags += 'D' 1283 if service.flapping: 1284 service.service_flags += 'F' 1285 if service.passiveonly: 1286 service.service_flags += 'P' 1287 1288 # Add host of service flags for status icons in treeview 1289 if host.acknowledged: 1290 service.host_flags += 'A' 1291 if host.scheduled_downtime: 1292 service.host_flags += 'D' 1293 if host.flapping: 1294 service.host_flags += 'F' 1295 if host.passiveonly: 1296 service.host_flags += 'P' 1297 1298 # find out if there has been some status change to notify user 1299 # compare sorted lists of filtered nagios items 1300 new_nagitems_filtered_list = [] 1301 1302 for i in self.nagitems_filtered['hosts'].values(): 1303 for h in i: 1304 new_nagitems_filtered_list.append((h.name, h.status)) 1305 for i in self.nagitems_filtered['services'].values(): 1306 for s in i: 1307 new_nagitems_filtered_list.append((s.host, s.name, s.status)) 1308 1309 # sort for better comparison 1310 new_nagitems_filtered_list.sort() 1311 1312 # in the following lines worst_status_diff only changes from UP to another value if there was some change in the 1313 # worst status - if it is the same as before it will just keep UP 1314 # if both lists are identical there was no status change 1315 if (self.nagitems_filtered_list == new_nagitems_filtered_list): 1316 self.worst_status_diff = 'UP' 1317 else: 1318 # if the new list is shorter than the first and there are no different hosts 1319 # there one host/service must have been recovered, which is not worth a notification 1320 diff = [] 1321 for i in new_nagitems_filtered_list: 1322 if i not in self.nagitems_filtered_list: 1323 # collect differences 1324 diff.append(i) 1325 if len(diff) == 0: 1326 self.worst_status_diff = 'UP' 1327 else: 1328 # if there are different hosts/services in list of new hosts there must be a notification 1329 # get list of states for comparison 1330 diff_states = [] 1331 for d in diff: 1332 diff_states.append(d[-1]) 1333 # temporary worst state index 1334 worst = 0 1335 for d in diff_states: 1336 # only check worst state if it is valid 1337 if d in STATES: 1338 if STATES.index(d) > worst: 1339 worst = STATES.index(d) 1340 1341 # final worst state is one of the predefined states 1342 self.worst_status_diff = STATES[worst] 1343 del diff_states 1344 1345 # get the current worst state, needed at least for systraystatusicon 1346 self.worst_status_last = self.worst_status_current 1347 self.worst_status_current = 'UP' 1348 if self.down > 0: 1349 self.worst_status_current = 'DOWN' 1350 elif self.unreachable > 0: 1351 self.worst_status_current = 'UNREACHABLE' 1352 elif self.disaster > 0: 1353 self.worst_status_current = 'DISASTER' 1354 elif self.critical > 0: 1355 self.worst_status_current = 'CRITICAL' 1356 elif self.high > 0: 1357 self.worst_status_current = 'HIGH' 1358 elif self.average > 0: 1359 self.worst_status_current = 'AVERAGE' 1360 elif self.warning > 0: 1361 self.worst_status_current = 'WARNING' 1362 elif self.information > 0: 1363 self.worst_status_current = 'INFORMATION' 1364 elif self.unknown > 0: 1365 self.worst_status_current = 'UNKNOWN' 1366 1367 # when everything is OK set this flag for GUI to evaluate 1368 if self.down == 0 and\ 1369 self.unreachable == 0 and\ 1370 self.disaster == 0 and\ 1371 self.unknown == 0 and\ 1372 self.critical == 0 and\ 1373 self.high == 0 and\ 1374 self.average == 0 and\ 1375 self.warning == 0 and\ 1376 self.information == 0: 1377 self.all_ok = True 1378 else: 1379 self.all_ok = False 1380 1381 # copy of listed nagitems for next comparison 1382 self.nagitems_filtered_list = copy.deepcopy(new_nagitems_filtered_list) 1383 del new_nagitems_filtered_list 1384 1385 # put new informations into respective dictionaries 1386 self.hosts = copy.deepcopy(self.new_hosts) 1387 self.new_hosts.clear() 1388 1389 # taken from GUI.RefreshDisplay() - get event history for notification 1390 # first clear current events 1391 self.events_current.clear() 1392 # get all nagitems 1393 for host in self.hosts.values(): 1394 if not host.status == 'UP': 1395 # only if host is not filtered out add it to current events 1396 # the boolean is meaningless for current events 1397 if host.visible: 1398 self.events_current[host.get_hash()] = True 1399 for service in host.services.values(): 1400 # same for services of host 1401 if service.visible: 1402 self.events_current[service.get_hash()] = True 1403 1404 # check if some cached event still is relevant - kick it out if not 1405 for event in list(self.events_history.keys()): 1406 if event not in self.events_current.keys(): 1407 self.events_history.pop(event) 1408 self.events_notification.pop(event) 1409 1410 # if some current event is not yet in event cache add it and mark it as fresh (=True) 1411 for event in list(self.events_current.keys()): 1412 if event not in self.events_history.keys() and conf.highlight_new_events: 1413 self.events_history[event] = True 1414 self.events_notification[event] = True 1415 1416 # after all checks are done unset checking flag 1417 self.isChecking = False 1418 1419 # return True if all worked well 1420 return Result() 1421 1422 def FetchURL(self, url, giveback='obj', cgi_data=None, no_auth=False, multipart=False): 1423 ''' 1424 get content of given url, cgi_data only used if present 1425 'obj' FetchURL gives back a dict full of miserable hosts/services, 1426 'xml' giving back as objectified xml 1427 'raw' it gives back pure HTML - useful for finding out IP or new version 1428 existence of cgi_data forces urllib to use POST instead of GET requests 1429 NEW: gives back a list containing result and, if necessary, a more clear error description 1430 ''' 1431 1432 # run this method which checks itself if there is some action to take for initializing connection 1433 # if no_auth is true do not use Auth headers, used by check for new version 1434 try: 1435 try: 1436 # debug 1437 if conf.debug_mode is True: 1438 # unpasswordify CGI data 1439 if cgi_data is not None and not isinstance(cgi_data, str): 1440 cgi_data_log = copy.copy(cgi_data) 1441 for key in cgi_data_log.keys(): 1442 if 'pass' in key: 1443 cgi_data_log[key] = '***************' 1444 else: 1445 cgi_data_log = cgi_data 1446 self.Debug(server=self.get_name(), debug='FetchURL: ' + url + ' CGI Data: ' + str(cgi_data_log)) 1447 1448 if OS == OS_DARWIN and self.cacert_path and not self.cacert_path.is_file(): 1449 # pyinstaller temp folder seems to be emptied completely after a while 1450 # so the directories containing the resources have to be recreated too 1451 self.cacert_path.parent.mkdir(exist_ok=True) 1452 # write cached content of cacert.pem file back onto disk 1453 with self.cacert_path.open(mode='wb') as file: 1454 file.write(self.cacert_content) 1455 1456 # in case we know the server's encoding use it 1457 if self.encoding: 1458 if cgi_data is not None: 1459 try: 1460 for k in cgi_data: 1461 cgi_data[k] = cgi_data[k].encode(self.encoding) 1462 except: 1463 # set to false to mark it as invalid 1464 self.encoding = False 1465 1466 # use session only for connections to monitor servers, other requests like looking for updates 1467 # should go out without credentials 1468 if no_auth is False and not self.refresh_authentication: 1469 # check if there is really a session 1470 if not self.session: 1471 self.reset_HTTP() 1472 self.init_HTTP() 1473 # most requests come without multipart/form-data 1474 if multipart is False: 1475 if cgi_data is None: 1476 response = self.session.get(url, timeout=self.timeout) 1477 else: 1478 response = self.session.post(url, data=cgi_data, timeout=self.timeout) 1479 else: 1480 # Checkmk and Opsview need multipart/form-data encoding 1481 # http://stackoverflow.com/questions/23120974/python-requests-post-multipart-form-data-without-filename-in-http-request#23131823 1482 form_data = dict() 1483 for key in cgi_data: 1484 form_data[key] = (None, cgi_data[key]) 1485 1486 # get response with cgi_data encodes as files 1487 response = self.session.post(url, files=form_data, timeout=self.timeout) 1488 else: 1489 # send request without authentication data 1490 temporary_session = requests.Session() 1491 temporary_session.headers['User-Agent'] = self.USER_AGENT 1492 1493 # add proxy information if necessary 1494 self.proxify(temporary_session) 1495 1496 # most requests come without multipart/form-data 1497 if multipart is False: 1498 if cgi_data is None: 1499 response = temporary_session.get(url, timeout=self.timeout) 1500 else: 1501 response = temporary_session.post(url, data=cgi_data, timeout=self.timeout) 1502 else: 1503 # Checkmk and Opsview need multipart/form-data encoding 1504 # http://stackoverflow.com/questions/23120974/python-requests-post-multipart-form-data-without-filename-in-http-request#23131823 1505 form_data = dict() 1506 for key in cgi_data: 1507 form_data[key] = (None, cgi_data[key]) 1508 # get response with cgi_data encodes as files 1509 response = temporary_session.post(url, files=form_data, timeout=self.timeout) 1510 1511 # cleanup 1512 del temporary_session 1513 1514 except Exception: 1515 traceback.print_exc(file=sys.stdout) 1516 result, error = self.Error(sys.exc_info()) 1517 if error.startswith('requests.exceptions.SSLError:'): 1518 self.tls_error = True 1519 else: 1520 self.tls_error = False 1521 return Result(result=result, error=error, status_code=-1) 1522 1523 # store encoding in case it is not set yet and is not False 1524 if self.encoding is None: 1525 self.encoding = response.encoding 1526 1527 # give back pure HTML or XML in case giveback is 'raw' 1528 if giveback == 'raw': 1529 # .text gives content in unicode 1530 return Result(result=response.text, 1531 status_code=response.status_code) 1532 1533 # objectified HTML 1534 if giveback == 'obj': 1535 yummysoup = BeautifulSoup(response.text, self.PARSER) 1536 return Result(result=yummysoup, status_code=response.status_code) 1537 1538 # objectified generic XML, valid at least for Opsview and Centreon 1539 elif giveback == 'xml': 1540 xmlobj = BeautifulSoup(response.text, self.PARSER) 1541 return Result(result=xmlobj, 1542 status_code=response.status_code) 1543 1544 except Exception: 1545 traceback.print_exc(file=sys.stdout) 1546 1547 result, error = self.Error(sys.exc_info()) 1548 return Result(result=result, error=error, status_code=response.status_code) 1549 1550 result, error = self.Error(sys.exc_info()) 1551 return Result(result=result, error=error, status_code=response.status_code) 1552 1553 def GetHost(self, host): 1554 ''' 1555 find out ip or hostname of given host to access hosts/devices which do not appear in DNS but 1556 have their ip saved in Nagios 1557 ''' 1558 1559 # the fasted method is taking hostname as used in monitor 1560 if conf.connect_by_host is True or host == '': 1561 return Result(result=host) 1562 1563 # initialize ip string 1564 ip = '' 1565 1566 # glue nagios cgi url and hostinfo 1567 cgiurl_host = self.monitor_cgi_url + '/extinfo.cgi?type=1&host=' + host 1568 1569 # get host info 1570 result = self.FetchURL(cgiurl_host, giveback='obj') 1571 htobj = result.result 1572 1573 try: 1574 # take ip from html soup 1575 ip = htobj.findAll(name='div', attrs={'class': 'data'})[-1].text 1576 1577 # workaround for URL-ified IP as described in SF bug 2967416 1578 # https://sourceforge.net/tracker/?func=detail&aid=2967416&group_id=236865&atid=1101370 1579 if '://' in ip: 1580 ip = ip.split('://')[1] 1581 1582 # last-minute-workaround for https://github.com/HenriWahl/Nagstamon/issues/48 1583 if ',' in ip: 1584 ip = ip.split(',')[0] 1585 1586 # print IP in debug mode 1587 if conf.debug_mode is True: 1588 self.Debug(server=self.get_name(), host=host, debug='IP of %s:' % (host) + ' ' + ip) 1589 # when connection by DNS is not configured do it by IP 1590 if conf.connect_by_dns is True: 1591 # try to get DNS name for ip, if not available use ip 1592 try: 1593 address = socket.gethostbyaddr(ip)[0] 1594 except socket.error: 1595 address = ip 1596 else: 1597 address = ip 1598 except Exception: 1599 result, error = self.Error(sys.exc_info()) 1600 return Result(result=result, error=error) 1601 1602 # do some cleanup 1603 del htobj 1604 1605 # give back host or ip 1606 return Result(result=address) 1607 1608 def GetItemsGenerator(self): 1609 ''' 1610 Generator for plain listing of all filtered items, used in QUI for tableview 1611 ''' 1612 1613 # reset number of filtered items 1614 self.nagitems_filtered_count = 0 1615 1616 for state in self.nagitems_filtered['hosts'].values(): 1617 for host in state: 1618 # increase number of items for use in table 1619 self.nagitems_filtered_count += 1 1620 yield (host) 1621 1622 for state in self.nagitems_filtered['services'].values(): 1623 for service in state: 1624 # increase number of items for use in table 1625 self.nagitems_filtered_count += 1 1626 yield (service) 1627 1628 def Hook(self): 1629 ''' 1630 allows to add some extra actions for a monitor server to be executed in RefreshLoop 1631 inspired by Centreon and its seemingly Alzheimer disease regarding session ID/Cookie/whatever 1632 ''' 1633 pass 1634 1635 def Error(self, error): 1636 ''' 1637 Handle errors somehow - print them or later log them into not yet existing log file 1638 ''' 1639 if conf.debug_mode: 1640 debug = '' 1641 for line in traceback.format_exception(error[0], error[1], error[2], 5): 1642 debug += line 1643 self.Debug(server=self.get_name(), debug=debug, head='ERROR') 1644 1645 return ['ERROR', traceback.format_exception_only(error[0], error[1])[0]] 1646 1647 def Debug(self, server='', host='', service='', debug='', head='DEBUG'): 1648 ''' 1649 centralized debugging 1650 ''' 1651 1652 # initialize items in line to be logged 1653 log_line = [head + ':', str(datetime.datetime.now())] 1654 if server != '': 1655 log_line.append(server) 1656 if host != '': 1657 log_line.append(host) 1658 if service != '': 1659 log_line.append(service) 1660 if debug != '': 1661 log_line.append(debug) 1662 1663 # put debug info into debug queue 1664 debug_queue.append(' '.join(log_line)) 1665 1666 def get_events_history_count(self): 1667 """ 1668 return number of unseen events - those which are set True as unseen 1669 """ 1670 return(len(list((e for e in self.events_history if self.events_history[e] is True)))) 1671 1672 def check_for_error(self, result, error, status_code): 1673 """ 1674 check if any error occured - if so, return error 1675 """ 1676 if error != '' or status_code > 400: 1677 return(Result(result=copy.deepcopy(result), 1678 error=copy.deepcopy(error), 1679 status_code=copy.deepcopy(status_code))) 1680 else: 1681 return(False) 1682 1683 def get_worst_status_current(self): 1684 """ 1685 hand over the current worst status for get_worst_status() 1686 """ 1687 # get the current worst state, needed at least for systraystatusicon 1688 self.worst_status_current = 'UP' 1689 if self.down > 0: 1690 self.worst_status_current = 'DOWN' 1691 elif self.unreachable > 0: 1692 self.worst_status_current = 'UNREACHABLE' 1693 elif self.disaster > 0: 1694 self.worst_status_current = 'DISASTER' 1695 elif self.critical > 0: 1696 self.worst_status_current = 'CRITICAL' 1697 elif self.high > 0: 1698 self.worst_status_current = 'HIGH' 1699 elif self.average > 0: 1700 self.worst_status_current = 'AVERAGE' 1701 elif self.warning > 0: 1702 self.worst_status_current = 'WARNING' 1703 elif self.information > 0: 1704 self.worst_status_current = 'INFORMATION' 1705 elif self.unknown > 0: 1706 self.worst_status_current = 'UNKNOWN' 1707 1708 return self.worst_status_current 1709 1710 def get_worst_status_diff(self): 1711 """ 1712 hand over the current worst status difference for QUI 1713 """ 1714 return self.worst_status_diff 1715