#!/usr/bin/python
# -*- encoding: utf-8; py-indent-offset: 4 -*-
# +------------------------------------------------------------------+
# |             ____ _               _        __  __ _  __           |
# |            / ___| |__   ___  ___| | __   |  \/  | |/ /           |
# |           | |   | '_ \ / _ \/ __| |/ /   | |\/| | ' /            |
# |           | |___| | | |  __/ (__|   <    | |  | | . \            |
# |            \____|_| |_|\___|\___|_|\_\___|_|  |_|_|\_\           |
# |                                                                  |
# | Copyright Mathias Kettner 2018             mk@mathias-kettner.de |
# +------------------------------------------------------------------+
#
# This file is part of Check_MK.
# The official homepage is at http://mathias-kettner.de/check_mk.
#
# check_mk 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 in version 2.  check_mk is  distributed
# in the hope that it will be useful, but WITHOUT ANY WARRANTY;  with-
# out even the implied warranty of  MERCHANTABILITY  or  FITNESS FOR A
# PARTICULAR PURPOSE. See the  GNU General Public License for more de-
# tails. You should have  received  a copy of the  GNU  General Public
# License along with GNU Make; see the file  COPYING.  If  not,  write
# to the Free Software Foundation, Inc., 51 Franklin St,  Fifth Floor,
# Boston, MA 02110-1301 USA.

# <<<systemd_units>>>
#   UNIT                                   LOAD   ACTIVE SUB    DESCRIPTION
# ● check-mk-enterprise-2018.07.24.service loaded failed failed LSB: OMD sites
# ● systemd-cryptsetup@cryptswap1.service  loaded failed failed Cryptography Setup for cryptswap1
# ● swapfile.swap                          loaded failed failed /swapfile
#
# LOAD   = Reflects whether the unit definition was properly loaded.
# ACTIVE = The high-level unit activation state, i.e. generalization of SUB.
# SUB    = The low-level unit activation state, values depend on unit type.
#
# 3 loaded units listed. Pass --all to see loaded but inactive units, too.
# To show all installed unit files use 'systemctl list-unit-files'.

#   .--Parse function------------------------------------------------------.
#   |  ____                        __                  _   _               |
#   | |  _ \ __ _ _ __ ___  ___   / _|_   _ _ __   ___| |_(_) ___  _ __    |
#   | | |_) / _` | '__/ __|/ _ \ | |_| | | | '_ \ / __| __| |/ _ \| '_ \   |
#   | |  __/ (_| | |  \__ \  __/ |  _| |_| | | | | (__| |_| | (_) | | | |  |
#   | |_|   \__,_|_|  |___/\___| |_|  \__,_|_| |_|\___|\__|_|\___/|_| |_|  |
#   |                                                                      |
#   '----------------------------------------------------------------------'

_SYSTEMD_UNITS = [
    '.service ',  # A service unit describes how to manage a service or application on the server. This will include how to start or stop the service, under which circumstances it should be automatically started, and the dependency and ordering information for related software.
    '.socket ',  # A socket unit file describes a network or IPC socket, or a FIFO buffer that systemd uses for socket-based activation. These always have an associated .service file that will be started when activity is seen on the socket that this unit defines.
    '.device ',  # A unit that describes a device that has been designated as needing systemd management by udev or the sysfs filesystem. Not all devices will have .device files. Some scenarios where .device units may be necessary are for ordering, mounting, and accessing the devices.
    '.mount ',  # This unit defines a mountpoint on the system to be managed by systemd. These are named after the mount path, with slashes changed to dashes. Entries within /etc/fstab can have units created automatically.
    '.automount ',  # An .automount unit configures a mountpoint that will be automatically mounted. These must be named after the mount point they refer to and must have a matching .mount unit to define the specifics of the mount.
    '.swap ',  # This unit describes swap space on the system. The name of these units must reflect the device or file path of the space.
    '.target ',  # A target unit is used to provide synchronization points for other units when booting up or changing states. They also can be used to bring the system to a new state. Other units specify their relation to targets to become tied to the target's operations.
    '.path ',  # This unit defines a path that can be used for path-based activation. By default, a .service unit of the same base name will be started when the path reaches the specified state. This uses inotify to monitor the path for changes.
    '.timer ',  # A .timer unit defines a timer that will be managed by systemd, similar to a cron job for delayed or scheduled activation. A matching unit will be started when the timer is reached.
    '.snapshot ',  # A .snapshot unit is created automatically by the systemctl snapshot command. It allows you to reconstruct the current state of the system after making changes. Snapshots do not survive across sessions and are used to roll back temporary states.
    '.slice ',  # A .slice unit is associated with Linux Control Group nodes, allowing resources to be restricted or assigned to any processes associated with the slice. The name reflects its hierarchical position within the cgroup tree. Units are placed in certain slices by default depending on their type.
    '.scope ',  # Scope units are created automatically by systemd from information received from its bus interfaces. These are used to manage sets of system processes that are created externally.
]


def parse_systemd_units(info):
    parsed = {}

    if not info:
        return {}
    iter_info = iter(info)
    target_states = {}

    line = next(iter_info)

    if line[0] == "[list-unit-files]":
        for line in iter_info:
            if line[0].startswith('['):
                break
            if len(line) >= 2:
                target_states[line[0]] = line[1]

    if line[0] == '[all]':
        try:
            line = next(iter_info)

        # no services listed
        except StopIteration:
            return parsed

        UnitEntry = collections.namedtuple(
            "UnitEntry", ['name', 'type', 'load', 'active', 'sub', 'description', 'state'])

        for row in iter_info:
            if row[0] == '*':
                row.pop(0)
            line = ' '.join(row)
            for unit_marker in _SYSTEMD_UNITS:
                utype = unit_marker.strip(' ')
                if utype in row[0]:
                    unit_type = unit_marker.strip('. ')
                    name, remains = line.split(unit_marker, 1)
                    if "@" in name:
                        pos = name.find("@")
                        temp = name[:pos + 1]
                    else:
                        temp = name
                    state = target_states.get("%s.%s" % (temp, unit_type), "unknown")
                    load, active, sub, descr = remains.split(' ', 3)
                    unit = UnitEntry(name, unit_type, load, active, sub, descr, state)
                    parsed.setdefault(unit.type, {})[unit.name] = unit
                    break
    return parsed


#.

check_info['systemd_units'] = {
    'parse_function': parse_systemd_units,
}

#   .--services------------------------------------------------------------.
#   |                                     _                                |
#   |                 ___  ___ _ ____   _(_) ___ ___  ___                  |
#   |                / __|/ _ \ '__\ \ / / |/ __/ _ \/ __|                 |
#   |                \__ \  __/ |   \ V /| | (_|  __/\__ \                 |
#   |                |___/\___|_|    \_/ |_|\___\___||___/                 |
#   |                                                                      |
#   '----------------------------------------------------------------------'

factory_settings["systemd_services_default_levels"] = {
    "states": {
        "active": 0,
        "inactive": 0,
        "failed": 2,
    },
    "states_default": 2,
    "else": 2,  # missleading name, used if service vanishes
}

discovery_systemd_units_services_rules = []


def discovery_systemd_units_services(parsed):
    services = parsed.get('service', {})

    def regex_match(what, name):
        if not what:
            return True
        for entry in what:
            if entry.startswith("~"):
                if regex(entry[1:]).match(name):
                    return True
                else:
                    continue
            elif entry == name:
                return True
        return False

    def state_match(rule_states, state):
        return any(s in (None, state) for s in rule_states)

    for settings in host_extra_conf(host_name(), discovery_systemd_units_services_rules):
        descriptions = settings.get("descriptions", [])
        names = settings.get("names", [])
        states = settings.get("states")
        for service in services.values():
            if (regex_match(descriptions, service.description) and
                    regex_match(names, service.name) and state_match(states, service.active)):
                yield service.name, {}


def check_systemd_units_services(item, params, parsed):
    services = parsed.get('service', {})
    service = services.get(item, None)
    if service is None:
        yield params["else"], "Service not found"
        return

    state = params["states"].get(service.active, params["states_default"])
    yield state, "Status: %s" % service.active
    yield 0, service.description


check_info['systemd_units.services'] = {
    'inventory_function': discovery_systemd_units_services,
    'check_function': check_systemd_units_services,
    'service_description': 'Systemd Service %s',
    'group': 'systemd_services',
    'default_levels_variable': 'systemd_services_default_levels',
}


def discovery_systemd_units_services_summary(parsed):
    if parsed:
        yield 'Summary', {}


def _services_split(services, blacklist):
    included = []
    excluded = []
    disabled = []
    activating = []
    static = []
    compiled_patterns = [regex(p) for p in blacklist]
    for service in services:
        if any(expr.match(service.name) for expr in compiled_patterns):
            excluded.append(service)
            continue
        if service.active == "activating":
            activating.append(service)
        elif service.state in ["disabled", "indirect"]:
            disabled.append(service)
        elif service.state == "static":
            static.append(service)
        else:
            included.append(service)
    return included, excluded, disabled, static, activating


def _check_activating_state(activating_services, params):
    previous_state = get_item_state("activating", {})
    now = int(time.time())
    current_state = {}
    for service in activating_services:
        activating_since = previous_state.get(service.name, now)
        current_state[service.name] = activating_since
        elapsed_time = now - activating_since
        levels = params.get("activating_levels")
        yield check_levels(elapsed_time,
                           None,
                           levels,
                           human_readable_func=get_age_human_readable,
                           infoname="Service '%s' activating for" % service.name)

        set_item_state("activating", current_state)


def _check_non_ok_services(systemd_services, params, output_string):
    servicenames_by_status = {}
    for service in systemd_services:
        servicenames_by_status.setdefault(service.active, []).append(service.name)

    for status, service_names in sorted(servicenames_by_status.iteritems()):
        state = params["states"].get(status, params["states_default"])
        if state == 0:
            continue

        count = len(service_names)
        services_text = ", ".join(sorted(service_names))
        info = output_string % (count, "" if count == 1 else "s", status, services_text)

        yield state, info


def check_systemd_units_services_summary(_no_item, params, parsed):
    services = parsed.get('service', {}).values()
    blacklist = params.get('ignored', [])
    yield 0, "%d services in total" % len(services)

    included, excluded, disabled, static, activating = _services_split(services, blacklist)

    included_template = "%d service%s %s (%s)"
    for subresult in _check_non_ok_services(included, params, included_template):
        yield subresult

    static_template = "%d static service%s %s (%s)"
    for subresult in _check_non_ok_services(static, params, static_template):
        yield subresult

    for subresult in _check_activating_state(activating, params):
        yield subresult

    if excluded:
        yield 0, "%d ignored services" % len(excluded)
    if disabled:
        yield 0, "%d disabled services" % len(disabled)


check_info['systemd_units.services_summary'] = {
    'inventory_function': discovery_systemd_units_services_summary,
    'check_function': check_systemd_units_services_summary,
    'service_description': 'Systemd Service %s',
    'group': 'systemd_services',
    'default_levels_variable': 'systemd_services_default_levels',
}

#.
