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