marcus67/little_brother

View on GitHub
little_brother/client_device_handler.py

Summary

Maintainability
D
1 day
Test Coverage
# -*- coding: utf-8 -*-

# Copyright (C) 2019-2022  Marcus Rickert
#
# See https://github.com/marcus67/little_brother
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

import little_brother.persistence.session_context
from little_brother import admin_event
from little_brother.persistence.persistent_dependency_injection_mix_in import PersistenceDependencyInjectionMixIn
from little_brother.process_handler import ProcessHandler
from little_brother.process_handler import ProcessHandlerConfigModel
from python_base_app import configuration
from python_base_app import log_handling
from python_base_app import stats
from python_base_app import tools
from python_base_app.configuration import ConfigurationException

SECTION_NAME = "ClientDeviceHandler"

CLIENT_DEVICE_SECTION_PREFIX = "ClientDevice"

DEFAULT_MIN_ACTIVITY_DURATION = 30  # seconds
DEFAULT_MAX_ACTIVE_PING_DELAY = 100  # milliseconds
DEFAULT_INACTIVE_FACTOR = 2
DEFAULT_SAMPLE_SIZE = 8
DEFAULT_SERVER_GROUP = "default-group"


class ClientDeviceHandlerConfigModel(ProcessHandlerConfigModel):

    def __init__(self):
        super(ClientDeviceHandlerConfigModel, self).__init__(p_section_name=SECTION_NAME)

        self.inactive_factor = DEFAULT_INACTIVE_FACTOR


class ClientDeviceConfigModel(configuration.ConfigModel):

    def __init__(self, p_section_name):
        super(ClientDeviceConfigModel, self).__init__(p_section_name=p_section_name)

        self.name = None
        self.username = None
        self.hostname = None

        self.min_activity_duration = DEFAULT_MIN_ACTIVITY_DURATION
        self.max_active_ping_delay = DEFAULT_MAX_ACTIVE_PING_DELAY
        self.sample_size = DEFAULT_SAMPLE_SIZE

    def __str__(self):
        return "ClientDevice (name=%s, user=%s, host=%s)" % (self.name, self.username, self.hostname)


class ClientDeviceSectionHandler(configuration.ConfigurationSectionHandler, PersistenceDependencyInjectionMixIn):

    def __init__(self):
        super(ClientDeviceSectionHandler, self).__init__(p_section_prefix=CLIENT_DEVICE_SECTION_PREFIX)
        self.client_device_configs = {}

    def handle_section(self, p_section_name):
        client_device_section = ClientDeviceConfigModel(p_section_name=p_section_name)

        self.scan(p_section=client_device_section)
        tools.check_config_value(p_config=client_device_section, p_config_attribute_name="username")
        tools.check_config_value(p_config=client_device_section, p_config_attribute_name="hostname")

        configs = self.client_device_configs.get(client_device_section.name)

        if configs is None:
            configs = []
            self.client_device_configs[client_device_section.name] = client_device_section

        configs.append(client_device_section)


class DeviceInfo(object):

    def __init__(self, p_device_name, p_max_active_ping_delay, p_min_activity_duration, p_sample_size,
                 p_hostname):

        self._device_name = p_device_name
        self._max_active_ping_delay = p_max_active_ping_delay
        self._min_activity_duration = p_min_activity_duration
        self._sample_size = p_sample_size
        self._hostname = p_hostname
        self._moving_average = None
        self._active = None
        self._response_time = None
        self._moving_average_response_time = None
        self._up_start_time = None

    @property
    def device_name(self):

        return self._device_name

    @property
    def hostname(self):

        return self._hostname

    def update_hostname(self, p_hostname):

        if self._hostname is not None and p_hostname != self._hostname:
            self.clear_moving_average()

        self._hostname = p_hostname

    def update_max_active_ping_delay(self, p_max_active_ping_delay):

        self._max_active_ping_delay = p_max_active_ping_delay

    def update_min_activity_duration(self, p_min_activity_duration):

        self._min_activity_duration = p_min_activity_duration

    def update_sample_size(self, p_sample_size):

        if self._sample_size is not None and p_sample_size != self._sample_size:
            self.clear_moving_average()

        self._sample_size = p_sample_size

    @property
    def moving_average_response_time(self):

        if self._moving_average is None:
            return None

        else:
            return self._moving_average.get_value()

    @property
    def response_time(self):

        if self._moving_average is None:
            return None

        else:
            return self._moving_average.get_latest_value()

    def add_ping_delay(self, p_reference_time, p_delay):

        if self._moving_average is None:
            self._moving_average = stats.MovingAverage(p_sample_size=self._sample_size)

        was_up = self.is_up

        self._moving_average.add_value(p_value=p_delay)

        if self.is_up and not was_up:
            self._up_start_time = p_reference_time

    def clear_moving_average(self):

        self._moving_average = None

    @property
    def is_active(self):
        return self._moving_average is not None

    @property
    def is_up(self):

        if not self.is_active:
            return False

        if self.moving_average_response_time is None:
            return False

        return self.moving_average_response_time < self._max_active_ping_delay

    def requires_process_start_event(self, p_reference_time):

        if not self.is_up:
            return False

        return (p_reference_time - self._up_start_time).total_seconds() >= self._min_activity_duration


class ClientDeviceHandler(PersistenceDependencyInjectionMixIn, ProcessHandler):

    def __init__(self, p_config, p_pinger=None):

        super().__init__(p_config=p_config)

        self._pinger = p_pinger

        self._logger = log_handling.get_logger(self.__class__.__name__)

        self._process_infos = {}
        self._device_infos = {}

    @property
    def device_infos(self):
        return self._device_infos

    def get_device_info(self, p_device_name):

        with little_brother.persistence.session_context.SessionContext(
                p_persistence=self.persistence) as session_context:
            device_info = self._device_infos.get(p_device_name)
            device = self.device_entity_manager.device_map(session_context).get(p_device_name)

            if device is not None:
                if device_info is None:
                    device_info = DeviceInfo(p_device_name=p_device_name,
                                             p_max_active_ping_delay=device.max_active_ping_delay,
                                             p_min_activity_duration=device.min_activity_duration,
                                             p_sample_size=device.sample_size,
                                             p_hostname=device.hostname)
                    self._device_infos[p_device_name] = device_info

                else:
                    device_info.update_max_active_ping_delay(device.max_active_ping_delay)
                    device_info.update_min_activity_duration(device.min_activity_duration)
                    device_info.update_hostname(device.hostname)
                    device_info.update_sample_size(device.sample_size)

            return device_info

    def ping_device(self, p_reference_time, p_device):

        fmt = "Pinging {device}..."
        self._logger.debug(fmt.format(device=p_device.hostname))

        delay = None

        try:
            delay = self._pinger.ping(p_host=p_device.hostname)

        except ConfigurationException as e:
            msg = "Exception '{exception}' while pinging device '{device}'! Is the DNS name correct?"
            self._logger.error(msg.format(exception=str(e), device=p_device.hostname))

        if delay is None:
            delay = self._config.inactive_factor * p_device.max_active_ping_delay

        device_info = self.get_device_info(p_device_name=p_device.device_name)

        device_info.add_ping_delay(p_reference_time=p_reference_time, p_delay=delay)

        fmt = "Moving average={delay:.0f}"
        self._logger.debug(fmt.format(delay=device_info.moving_average_response_time))

        fmt = "{device} is {status}"
        self._logger.debug(fmt.format(device=p_device.device_name, status="up" if device_info.is_up else "down"))

    def get_current_active_pinfo(self, p_hostname, p_username):

        max_start_time = None
        most_recent_pinfo = None

        for pinfo in self._process_infos.values():
            if pinfo.hostname == p_hostname and pinfo.username == p_username and \
                    (max_start_time is None or pinfo.start_time > max_start_time):
                max_start_time = pinfo.start_time
                most_recent_pinfo = pinfo

        if most_recent_pinfo is not None and most_recent_pinfo.end_time is None:
            return most_recent_pinfo

        return None

    def get_number_of_monitored_devices(self):

        with little_brother.persistence.session_context.SessionContext(
                p_persistence=self.persistence) as session_context:
            return len(self.device_entity_manager.devices(session_context))

    def scan_processes(self, p_session_context, p_reference_time, p_server_group, p_login_mapping, p_host_name,
                       p_process_regex_map, p_prohibited_process_regex_map):

        events = []

        for device in self.device_entity_manager.devices(p_session_context):
            if device.hostname is not None:
                self.ping_device(p_reference_time=p_reference_time, p_device=device)

        for device_info in self._device_infos.values():
            if device_info.device_name not in self.device_entity_manager.device_map(
                    p_session_context=p_session_context):
                # Clear statistics for old device names so that they are correctly reported in Prometheus
                device_info.clear_moving_average()

        for device in self.device_entity_manager.devices(p_session_context=p_session_context):
            device_info = self.get_device_info(p_device_name=device.device_name)

            if device_info.requires_process_start_event(p_reference_time=p_reference_time):
                # Send process start event for monitored users (if required)
                for user2device in device.users:
                    current_pinfo = self.get_current_active_pinfo(device.hostname,
                                                                  p_username=user2device.user.username)

                    if user2device.active:
                        if current_pinfo is None:
                            event = admin_event.AdminEvent(
                                p_event_type=admin_event.EVENT_TYPE_PROCESS_START,
                                p_hostname=device_info.hostname,
                                p_hostlabel=device_info.device_name,
                                p_processhandler=self.id,
                                p_username=user2device.user.username,
                                p_percent=user2device.percent,
                                p_process_start_time=p_reference_time)
                            events.append(event)

                    else:
                        if current_pinfo is not None:
                            event = self.create_admin_event_process_end_from_pinfo(
                                p_pinfo=current_pinfo,
                                p_reference_time=p_reference_time)
                            events.append(event)


            else:
                # Send process stop event for non monitored users (if required)
                for user2device in device.users:
                    current_pinfo = self.get_current_active_pinfo(device.hostname,
                                                                  p_username=user2device.user.username)

                    if current_pinfo is not None:
                        event = self.create_admin_event_process_end_from_pinfo(
                            p_pinfo=current_pinfo,
                            p_reference_time=p_reference_time)
                        events.append(event)

        active_hostnames = [device_info.hostname for device_info in self._device_infos.values() if
                            device_info.is_up]

        for pinfo in self._process_infos.values():
            # If the end time of a current entry is None AND the process was started on the local host AND
            # the process is no longer running THEN send an EVENT_TYPE_PROCESS_END event!
            if pinfo.end_time is None and pinfo.hostname not in active_hostnames:
                event = self.create_admin_event_process_end_from_pinfo(
                    p_pinfo=pinfo,
                    p_reference_time=p_reference_time)
                events.append(event)

        return events