#!/usr/bin/env python3
# -*- coding: utf-8; py-indent-offset: 4 -*-
#
# Author:  Linuxfabrik GmbH, Zurich, Switzerland
# Contact: info (at) linuxfabrik (dot) ch
#          https://www.linuxfabrik.ch/
# License: The Unlicense, see LICENSE file.

# https://github.com/Linuxfabrik/monitoring-plugins/blob/main/CONTRIBUTING.md

"""See the check's README for more details."""

import argparse
import os
import re
import socket
import sys

import lib.args
import lib.base
import lib.disk
import lib.distro
import lib.dmidecode
import lib.human
import lib.net
import lib.shell
import lib.time
import lib.txt
from lib.globals import STATE_OK, STATE_UNKNOWN

try:
    import psutil

    HAVE_PSUTIL = True
except ImportError:
    HAVE_PSUTIL = False


__author__ = 'Linuxfabrik GmbH, Zurich/Switzerland'
__version__ = '2026050501 / v3.0.0'

DESCRIPTION = """Collects and displays key system information: OS and kernel version, CPU
configuration (physical, logical, and usable cores plus frequency), RAM, disk count,
virtualization type, network interfaces, listening ports, systemd services and timers,
cron jobs, installed packages, and user accounts. Optionally queries dmidecode for
firmware and hardware details, and fetches the public IP address. This check is purely
informational and never raises alerts.
Requires root or sudo."""

DEFAULT_DMIDECODE = False
DEFAULT_INSECURE = False
DEFAULT_NO_PROXY = False
DEFAULT_PUBLIC_IP_URL = None
DEFAULT_TIMEOUT = 2


def parse_args():
    """Parse command line arguments using argparse."""
    parser = argparse.ArgumentParser(description=DESCRIPTION)

    parser.add_argument(
        '-V',
        '--version',
        action='version',
        version=f'%(prog)s: v{__version__} by {__author__}',
    )

    parser.add_argument(
        '--dmidecode',
        help='Gather additional hardware information via the `dmidecode` command, '
        'such as system components, serial numbers, and BIOS revisions. '
        'Requires sudo permissions.',
        dest='DMIDECODE',
        action='store_true',
        default=DEFAULT_DMIDECODE,
    )

    parser.add_argument(
        '--insecure',
        help=lib.args.help('--insecure'),
        dest='INSECURE',
        action='store_true',
        default=DEFAULT_INSECURE,
    )

    parser.add_argument(
        '--no-proxy',
        help=lib.args.help('--no-proxy'),
        dest='NO_PROXY',
        action='store_true',
        default=DEFAULT_NO_PROXY,
    )

    parser.add_argument(
        '--public-ip-url',
        help='One or more comma-separated URLs to "what is my ip" online services '
        'for fetching the public IP address. '
        'Example: '
        '`https://ipv4.icanhazip.com,https://ipecho.net/plain,https://ipinfo.io/ip`. '
        'Default: %(default)s',
        dest='PUBLIC_IP_URL',
        default=DEFAULT_PUBLIC_IP_URL,
    )

    parser.add_argument(
        '--tags',
        help='Guess a list of tags to apply in Icinga Director (Linuxfabrik Basket config).',
        dest='TAGS',
        action='store_true',
        default=False,
    )

    parser.add_argument(
        '--timeout',
        help=lib.args.help('--timeout') + ' Default: %(default)s (seconds)',
        dest='TIMEOUT',
        type=int,
        default=DEFAULT_TIMEOUT,
    )

    args, _ = parser.parse_known_args()
    return args


def get_birthday():
    """Using various methods to determine install date."""
    # the age of a machine is usually determined by the birthday of the root file system `/`.
    # however, this does not work for cloud systems that are installed from pre-built images.
    # in this case, the age of the cloud-init data must be used.
    if os.path.exists('/var/lib/cloud/data/instance-id'):
        # cloud-init based VM
        birthday = os.stat('/var/lib/cloud/data/instance-id')
        return f'born {lib.time.epoch2iso(birthday.st_ctime)}. '
    # no way to do this in python - getting the birthday of a folder
    # hardcoded shell pipeline (no user input); shell=True needed for pipes
    _success, result = lib.shell.shell_exec(
        'stat / | grep "Birth" | sed "s/Birth: //g" | cut -b 2-11',
        shell=True,  # nosec B604
    )
    birthday, _, _ = result
    birthday = birthday.strip()
    return f'born {birthday}. '


def get_boot_mode():
    return 'UEFI boot, ' if os.path.isdir('/sys/firmware/efi') else 'BIOS boot, '


def get_crontab():
    """Returns all crontab items."""
    cmd = r"grep --dereference-recursive --no-filename --invert-match '\s*#' /etc/crontab /etc/cron.d/ /etc/anacrontab /var/spool/cron"
    success, result = lib.shell.shell_exec(cmd)
    if not success:
        return ''
    stdout, _, _ = result
    output = ''
    line_regex = re.compile(r'\S+=')
    for line in stdout.splitlines():
        line = line.strip()
        if len(line) > 0 and re.match(line_regex, line) is None:
            output = f'{output}{line}\n'
    if output:
        output = f'crontab:\n{output}\n'
    return output


def get_disks():
    """Returns system disks tuple "text, count": `'Disk nvme0n1 1.8T', 1`"""
    success, result = lib.shell.shell_exec(
        'lsblk --nodeps --output NAME,SIZE --noheadings --include 8,252,259'
    )
    if not success:
        return '', 0
    stdout, _, _ = result
    output = []
    for disk in stdout.strip().splitlines():
        # zRAM devices can appear in the output of lsblk, but we cannot do anything useful with them
        if disk.startswith('zram'):
            continue
        output.append(re.sub('\\s+', ' ', disk))
    return f'{lib.txt.pluralize("Disk", len(output))} {", ".join(output)}, ', len(
        output
    )


def get_hw_info():
    result = lib.dmidecode.get_data()
    if not result:
        return ''
    output = ''
    for _, value in result.items():
        if value['dminame'] == 'Base Board Information':
            d = 'Default string'
            output += (
                f'* Base Board: Type'
                f' {value.get("Type", "n/a").replace(d, "n/a")}'
                f' {value.get("Manufacturer", "n/a").replace(d, "n/a")}'
                f' {value.get("Product Name", "n/a").replace(d, "n/a")}'
                f', SerNo {value.get("Serial Number", "n/a").replace(d, "n/a")}'
                f', Ver {value.get("Version", "n/a").replace(d, "n/a")}\n'
            )
        if value['dminame'] == 'BIOS Information':
            d = 'Default string'
            output += (
                f'* BIOS: {value.get("Vendor", "n/a").replace(d, "n/a")}'
                f', Ver {value.get("Version", "n/a").replace(d, "n/a")}'
                f' (released {value.get("Release Date", "n/a").replace(d, "n/a")})'
                f', ROM {value.get("ROM Size", "n/a").replace(d, "n/a")}\n'
            )
        if value['dminame'] == 'Chassis Information':
            d = 'Default string'
            output += (
                f'* Chassis:'
                f' {value.get("Manufacturer", "n/a").replace(d, "n/a")}'
                f', Type {value.get("Type", "n/a").replace(d, "n/a")}'
                f', SKU {value.get("SKU", "n/a").replace(d, "n/a")}'
                f', SerNo {value.get("Serial Number", "n/a").replace(d, "n/a")}\n'
            )
            output += (
                f'  States:'
                f' boot-up={value.get("Boot-up State", "n/a").replace(d, "n/a")}'
                f', pwr-supply={value.get("Power Supply State", "n/a").replace(d, "n/a")}'
                f', thermal={value.get("Thermal State", "n/a").replace(d, "n/a")}'
                f', security={value.get("Security Status", "n/a").replace(d, "n/a")}\n'
            )
        if value['dminame'] == 'Processor Information':
            d = 'Default string'
            output += (
                f'* Proc: {value.get("Manufacturer", "n/a").replace(d, "n/a")}'
                f', Ver {value.get("Version", "n/a").replace(d, "n/a")},\n'
            )
            output += (
                f'  Speed {value.get("Current Speed", "n/a").replace(d, "n/a")}'
                f'/{value.get("Max Speed", "n/a").replace(d, "n/a")} max.'
                f', {value.get("Core Enabled", "n/a").replace(d, "n/a")}'
                f'/{value.get("Core Count", "n/a").replace(d, "n/a")} Cores enabled'
                f', {value.get("Thread Count", "n/a").replace(d, "n/a")}'
                f' {lib.txt.pluralize("Thread", value.get("Thread Count", 0))}'
                f', Voltage {value.get("Voltage", "n/a").replace(d, "n/a")}\n'
            )
        if value['dminame'] == 'System Boot Information':
            d = 'Default string'
            output += f'* System Boot: {value.get("Status", "n/a").replace(d, "n/a")}\n'
        if value['dminame'] == 'System Information':
            d = 'Default string'
            output += (
                f'* SysInfo:'
                f' {value.get("Manufacturer", "n/a").replace(d, "n/a")}'
                f' {value.get("Product Name", "n/a").replace(d, "n/a")}'
                f', SerNo {value.get("Serial Number", "n/a").replace(d, "n/a")}'
                f', SKU {value.get("SKU", "n/a").replace(d, "n/a")}'
                f', Wake-up Type "{value.get("Wake-up Type", "n/a").replace(d, "n/a")}",\n'
            )
            output += (
                f'  UUID {value.get("UUID", "n/a").replace("Default string", "n/a")}\n'
            )
    if output:
        return f'Hardware Info (`dmidecode`):\n{output}\n'
    return output


def get_interfaces():
    output = ''
    try:
        for name, interface in sorted(psutil.net_if_addrs().items()):
            if name == 'lo':
                continue
            for addr in interface:
                if addr.family == lib.net.AF_INET:
                    output += (
                        f'* {name} {addr.address}/{lib.net.ip_to_cidr(addr.netmask)}\n'
                    )
    except Exception:
        pass
    if output:
        return f'Interfaces (IPv4):\n{output}\n'
    return output


def get_lftags(distro):
    """Try to automatically detect as many Linuxfabrik tags for Icinga Director as possible.
    Tags that are disabled here can't be detected on the system running this plugin, but are present
    in all-the-rest.json. So keep this function in sync with all-the-rest.json.
    Also, if you need to add software here that isn't installed by a package manager and therefore
    can only be guessed, put it in get_software_found() as well.
    """
    lftags = [
        # {'sw': 'LF Tag Name', 'package': ['pname1', 'pname2'], 'expr': [stmnt1, stmnt2, ...]},
        {
            'sw': 'acme.sh',
            'package': [],
            'expr': [run_cmd('systemctl is-enabled acme-sh.timer', ignore_output=True)],
        },
        {
            'sw': 'AIDE',
            'package': [],
            'expr': [
                run_cmd('systemctl is-enabled aide-check.timer', ignore_output=True)
            ],
        },
        {
            'sw': 'Apache httpd',
            'package': [],
            'expr': [
                run_cmd('systemctl is-enabled httpd.service', ignore_output=True),
                run_cmd('systemctl is-enabled apache2.service', ignore_output=True),
            ],
        },
        {
            'sw': 'Apache Solr',
            'package': [],
            'expr': [run_cmd('systemctl is-enabled solr.service', ignore_output=True)],
        },
        # {'sw': 'Axenita', 'package': [], 'expr': []},
        {
            'sw': 'BIND',
            'package': [],
            'expr': [run_cmd('systemctl is-enabled named.service', ignore_output=True)],
        },
        {'sw': 'Bonding', 'package': [], 'expr': [os.path.isdir('/proc/net/bonding')]},
        {
            'sw': 'BorgBackup',
            'package': [],
            'expr': [
                run_cmd(
                    'systemctl is-enabled borg-backup-daily.timer', ignore_output=True
                )
            ],
        },
        {
            'sw': 'Chronyd',
            'package': [],
            'expr': [
                run_cmd('systemctl is-enabled chronyd.service', ignore_output=True)
            ],
        },
        {
            'sw': 'ClamAV',
            'package': [],
            'expr': [
                run_cmd('systemctl is-enabled clamd@scan.service', ignore_output=True)
            ],
        },
        {
            'sw': 'Collabora Online',
            'package': [],
            'expr': [
                run_cmd('systemctl is-enabled coolwsd.service', ignore_output=True)
            ],
        },
        {'sw': 'Composer', 'package': ['composer'], 'expr': []},
        {
            'sw': 'coturn',
            'package': ['coturn'],
            'expr': [run_cmd('systemctl is-enabled coturn.service')],
        },
        # {'sw': 'DHCP Client', 'package': [], 'expr': []},
        {
            'sw': 'Docker',
            'package': [],
            'expr': [
                run_cmd('systemctl is-enabled docker.service', ignore_output=True)
            ],
        },
        {
            'sw': 'Duplicity',
            'package': [],
            'expr': [run_cmd('systemctl is-enabled duba.timer', ignore_output=True)],
        },
        {
            'sw': 'Elasticsearch',
            'package': [],
            'expr': [
                run_cmd(
                    'systemctl is-enabled elasticsearch.service', ignore_output=True
                )
            ],
        },
        {
            'sw': 'Exim4',
            'package': [],
            'expr': [run_cmd('systemctl is-enabled exim4.service', ignore_output=True)],
        },
        {
            'sw': 'Fail2Ban',
            'package': [],
            'expr': [
                run_cmd('systemctl is-enabled fail2ban.service', ignore_output=True)
            ],
        },
        {
            'sw': 'FirewallD',
            'package': [],
            'expr': [
                run_cmd('systemctl is-enabled firewalld.service', ignore_output=True)
            ],
        },
        # {'sw': 'FortiOS 6', 'package': [], 'expr': []},
        {
            'sw': 'FreeIPA Server',
            'package': [],
            'expr': [run_cmd('systemctl is-enabled ipa.service', ignore_output=True)],
        },
        {
            'sw': 'Fwbuilder',
            'package': [],
            'expr': [run_cmd('systemctl is-enabled fwb.service', ignore_output=True)],
        },
        {
            'sw': 'GitLab',
            'package': [],
            'expr': [
                run_cmd(
                    'systemctl is-enabled gitlab-runsvdir.service', ignore_output=True
                )
            ],
        },
        {
            'sw': 'Gluster Host',
            'package': [],
            'expr': [
                run_cmd('systemctl is-enabled glusterd.service', ignore_output=True)
            ],
        },
        {
            'sw': 'Grafana',
            'package': [],
            'expr': [
                run_cmd(
                    'systemctl is-enabled grafana-server.service', ignore_output=True
                )
            ],
        },
        {
            'sw': 'Grav',
            'package': [],
            'expr': [
                run_cmd(
                    'systemctl is-enabled grav-selfupgrade.timer', ignore_output=True
                )
            ],
        },
        {
            'sw': 'Graylog Server',
            'package': [],
            'expr': [
                run_cmd(
                    'systemctl is-enabled graylog-server.service', ignore_output=True
                )
            ],
        },
        {
            'sw': 'H-Net eFaktura',
            'package': [],
            'expr': [
                run_cmd(
                    'systemctl is-enabled hnet-securesvc.service', ignore_output=True
                )
            ],
        },
        {
            'sw': 'HAProxy',
            'package': [],
            'expr': [
                run_cmd('systemctl is-enabled haproxy.service', ignore_output=True)
            ],
        },
        # {'sw': 'Huawei Dorado', 'package': [], 'expr': []},
        {
            'sw': 'Icinga2',
            'package': [],
            'expr': [
                run_cmd('systemctl is-enabled icinga2.service', ignore_output=True)
            ],
        },
        {
            'sw': 'IcingaDB',
            'package': [],
            'expr': [
                run_cmd('systemctl is-enabled icingadb.service', ignore_output=True)
            ],
        },
        {
            'sw': 'InfluxDB',
            'package': [],
            'expr': [
                run_cmd('systemctl is-enabled influxdb.service', ignore_output=True)
            ],
        },
        # {'sw': 'Infomaniak Swiss Backup', 'package': [], 'expr': []},
        {'sw': 'IPMI', 'package': ['ipmitool'], 'expr': []},
        {
            'sw': 'iSCSI',
            'package': [],
            'expr': [run_cmd('systemctl is-enabled iscsi.service', ignore_output=True)],
        },
        # {'sw': 'Jitsi', 'package': [], 'expr': []},
        {
            'sw': 'JumpCloud Agent',
            'package': [],
            'expr': [
                run_cmd('systemctl is-enabled jcagent.service', ignore_output=True)
            ],
        },
        # {'sw': 'KEMP', 'package': [], 'expr': []},
        {
            'sw': 'Keycloak',
            'package': [],
            'expr': [
                run_cmd('systemctl is-enabled keycloak.service', ignore_output=True)
            ],
        },
        {
            'sw': 'KVM Host',
            'package': [],
            'expr': [
                run_cmd('systemctl is-enabled libvirtd.service', ignore_output=True)
            ],
        },
        {'sw': 'LibreNMS', 'package': [], 'expr': [os.path.isdir('/opt/librenms')]},
        {
            'sw': 'Logstash Client',
            'package': [],
            'expr': [
                run_cmd('systemctl is-enabled filebeat.service', ignore_output=True)
            ],
        },
        {
            'sw': 'Logstash Server',
            'package': [],
            'expr': [
                run_cmd('systemctl is-enabled logstash.service', ignore_output=True)
            ],
        },
        {
            'sw': 'MariaDB',
            'package': [],
            'expr': [
                run_cmd('systemctl is-enabled mariadb.service', ignore_output=True)
            ],
        },
        {
            'sw': 'MariaDB Dump',
            'package': [],
            'expr': [
                run_cmd('systemctl is-enabled mariadb-dump.timer', ignore_output=True)
            ],
        },
        # {'sw': 'MariaDB InnoDB', 'package': [], 'expr': []},
        # {'sw': 'MariaDB Metrics', 'package': [], 'expr': []},
        # {'sw': 'MariaDB Replication', 'package': [], 'expr': []},
        # {'sw': 'MariaDB Schemas', 'package': [], 'expr': []},
        # {'sw': 'MariaDB Security', 'package': [], 'expr': []},
        {
            'sw': 'Mastodon',
            'package': [],
            'expr': [
                run_cmd(
                    'systemctl is-enabled mastodon-sidekiq.service', ignore_output=True
                ),
                run_cmd(
                    'systemctl is-enabled mastodon-streaming.service',
                    ignore_output=True,
                ),
                run_cmd(
                    'systemctl is-enabled mastodon-web.service', ignore_output=True
                ),
            ],
        },
        {
            'sw': 'Matomo',
            'package': [],
            'expr': [
                os.path.isdir('/var/www/matomo'),
                os.path.isdir('/var/www/html/matomo'),
                os.path.isdir('/var/www/html/piwik'),
            ],
        },
        {
            'sw': 'MediaWiki',
            'package': [],
            'expr': [
                os.path.isdir('/var/www/html/mediawiki'),
                os.path.isdir('/var/www/html/wiki'),
                os.path.isdir('/var/www/mediawiki'),
                os.path.isdir('/var/www/wiki'),
            ],
        },
        {'sw': 'Metabase', 'package': [], 'expr': [os.path.isdir('/opt/metabase')]},
        {
            'sw': 'mod_qos',
            'package': ['mod_qos'],
            # hardcoded shell pipelines (no user input); shell=True needed for pipes
            'expr': [
                os.path.isfile('/usr/lib64/httpd/modules/mod_qos.so'),
                os.path.isdir('/var/lib/mod_security'),
                run_cmd(
                    'httpd -t -D DUMP_MODULES | grep mod_qos',
                    shell=True,  # nosec B604
                    ignore_output=True,
                ),
                run_cmd(
                    'apache2 -t -D DUMP_MODULES | grep mod_qos',
                    shell=True,  # nosec B604
                    ignore_output=True,
                ),
            ],
        },
        {
            'sw': 'MongoDB',
            'package': [],
            'expr': [
                run_cmd('systemctl is-enabled mongod.service', ignore_output=True)
            ],
        },
        {
            'sw': 'Moodle',
            'package': [],
            'expr': [
                run_cmd('systemctl is-enabled moodle-cron.timer', ignore_output=True)
            ],
        },
        {
            'sw': 'mydumper',
            'package': ['mydumper'],
            'expr': [
                os.path.isfile('/etc/mydumper.cnf'),
                os.path.isfile('/usr/bin/mydumper'),
            ],
        },
        {
            'sw': 'MySQL',
            'package': [],
            'expr': [run_cmd('systemctl is-enabled mysql.service', ignore_output=True)],
        },
        # {'sw': 'MySQL InnoDB', 'package': [], 'expr': []},
        # {'sw': 'MySQL Metrics', 'package': [], 'expr': []},
        # {'sw': 'MySQL Replication', 'package': [], 'expr': []},
        # {'sw': 'MySQL Schemas', 'package': [], 'expr': []},
        # {'sw': 'MySQL Security', 'package': [], 'expr': []},
        {
            'sw': 'networking.service',
            'package': [],
            'expr': [
                run_cmd('systemctl is-enabled networking.service', ignore_output=True)
            ],
        },
        {
            'sw': 'Nextcloud',
            'package': [],
            'expr': [
                run_cmd('systemctl is-enabled nextcloud-jobs.timer', ignore_output=True)
            ],
        },
        {
            'sw': 'NFS Server',
            'package': [],
            'expr': [
                run_cmd('systemctl is-enabled nfs-server.service', ignore_output=True)
            ],
        },
        {
            'sw': 'Nginx',
            'package': [],
            'expr': [run_cmd('systemctl is-enabled nginx.service', ignore_output=True)],
        },
        {'sw': 'NodeBB', 'package': [], 'expr': [os.path.isdir('/opt/nodebb')]},
        {
            'sw': 'NTPd',
            'package': [],
            'expr': [run_cmd('systemctl is-enabled ntpd.service', ignore_output=True)],
        },
        {
            'sw': 'OnlyOffice',
            'package': [],
            'expr': [os.path.isdir('/var/log/onlyoffice')],
        },
        {
            'sw': 'OpenVAS',
            'package': [],
            'expr': [run_cmd('systemctl is-enabled gsad.service', ignore_output=True)],
        },
        {
            'sw': 'OpenVPN Server',
            'package': ['openvpn'],
            'expr': [
                run_cmd(
                    'systemctl is-enabled openvpn-server@server.service',
                    ignore_output=True,
                )
            ],
        },
        {
            'sw': f'OS - {distro.get("os_info")}, family "{distro.get("os_family")}"',
            'package': [],
            'expr': [True],  # always check this
        },
        {'sw': 'PHP', 'package': ['php'], 'expr': [os.path.isfile('/etc/php.ini')]},
        {
            'sw': 'PHP-FPM',
            'package': [],
            'expr': [
                run_cmd('systemctl is-enabled php-fpm.service', ignore_output=True)
            ],
        },
        {
            'sw': 'pip',
            'package': ['pip3', 'pip2', 'python3-pip', 'python2-pip'],
            'expr': [],
        },
        {
            'sw': 'Postfix MTA',
            'package': [],
            'expr': [
                run_cmd('systemctl is-enabled postfix.service', ignore_output=True)
            ],
        },
        {
            'sw': 'PostgreSQL',
            'package': [],
            'expr': [
                run_cmd('systemctl is-enabled postgresql.service', ignore_output=True)
            ],
        },
        # {'sw': 'Proxmox', 'package': [], 'expr': []},
        {'sw': 'Python', 'package': ['python3', 'python2'], 'expr': []},
        # {'sw': 'QNAP QTS', 'package': [], 'expr': []},
        {
            'sw': 'RabbitMQ Server',
            'package': [],
            'expr': [
                run_cmd(
                    'systemctl is-enabled rabbitmq-server.service', ignore_output=True
                )
            ],
        },
        # {'sw': 'Redfish', 'package': [], 'expr': []},
        {
            'sw': 'Redis',
            'package': [],
            'expr': [run_cmd('systemctl is-enabled redis.service', ignore_output=True)],
        },
        {
            'sw': 'restic',
            'package': ['restic'],
            'expr': [run_cmd('restic version', ignore_output=True)],
        },
        {
            'sw': 'Rocket.Chat',
            'package': [],
            'expr': [
                run_cmd('systemctl is-enabled rocketchat.service', ignore_output=True)
            ],
        },
        {
            'sw': 'rsyncd',
            'package': [],
            'expr': [
                run_cmd('systemctl is-enabled rsyncd.service', ignore_output=True)
            ],
        },
        # {'sw': 'SafeNet HSM', 'package': [], 'expr': []},
        {
            'sw': 'rsyslog',
            'package': [],
            'expr': [
                run_cmd('systemctl is-enabled rsyslog.service', ignore_output=True)
            ],
        },
        {
            'sw': 'Samba',
            'package': [],
            'expr': [run_cmd('systemctl is-enabled smb.service', ignore_output=True)],
        },
        {'sw': 'Scanrootkit', 'package': [], 'expr': [True]},
        {
            'sw': 'snmpd',
            'package': [],
            'expr': [run_cmd('systemctl is-enabled snmpd.service', ignore_output=True)],
        },
        {
            'sw': 'Splunk',
            'package': [],
            'expr': [
                run_cmd('systemctl is-enabled splunk.service', ignore_output=True)
            ],
        },
        # {'sw': 'Starface PBX', 'package': [], 'expr': []},
        # {'sw': 'Statuspal', 'package': [], 'expr': []},
        {
            'sw': 'strongSwan IPSec',
            'package': [],
            'expr': [
                run_cmd('systemctl is-enabled strongswan.service', ignore_output=True)
            ],
        },
        {
            'sw': 'syslog-ng',
            'package': [],
            'expr': [
                run_cmd('systemctl is-enabled syslog-ng.service', ignore_output=True)
            ],
        },
        {
            'sw': 'System Update',
            'package': [],
            'expr': [
                run_cmd(
                    'systemctl is-enabled notify-and-schedule.timer', ignore_output=True
                )
            ],
        },
        {
            'sw': 'Systemd Timesyncd',
            'package': [],
            'expr': [
                run_cmd(
                    'systemctl is-enabled systemd-timesyncd.service', ignore_output=True
                )
            ],
        },
        {
            'sw': 'TuneD',
            'package': [],
            'expr': [run_cmd('systemctl is-enabled tuned.service', ignore_output=True)],
        },
        {
            'sw': 'UPS (Network UPS Tools, nut)',
            'package': [],
            'expr': [
                run_cmd('systemctl is-enabled nut-server.service', ignore_output=True)
            ],
        },
        {
            'sw': 'Valkey',
            'package': [],
            'expr': [
                run_cmd('systemctl is-enabled valkey.service', ignore_output=True)
            ],
        },
        {
            'sw': 'vsftpd',
            'package': [],
            'expr': [
                run_cmd('systemctl is-enabled vsftpd.service', ignore_output=True)
            ],
        },
        {'sw': 'WHMCS', 'package': [], 'expr': [os.path.isdir('/home/whmcs')]},
        {'sw': 'Wildfly', 'package': [], 'expr': [os.path.isdir('/opt/wildfly')]},
        {
            'sw': 'Wordpress',
            'package': [],
            'expr': [
                os.path.isdir('/var/www/html/wordpress'),
                os.path.isdir('/var/www/html/wp-config.php'),
                os.path.isdir('/var/www/wordpress'),
            ],
        },
        # {'sw': 'XFS', 'package': [], 'expr': []},
    ]
    success, result = lib.shell.shell_exec(
        'dnf repoquery --userinstalled --queryformat "%{name}"'
    )
    if success:
        # for the moment focusing on rhel-compatible package managers
        userinstalled_software, _, _ = result
        userinstalled_software = userinstalled_software.splitlines()
    else:
        userinstalled_software = []

    output = ''
    for item in lftags:
        # if any of the listed software packages is installed, we have a match
        if any(i in item['package'] for i in userinstalled_software) or any(
            item['expr']
        ):
            output += f'* {item["sw"]}\n'
    if output:
        return f"Linuxfabrik's Icinga Director Tags:\n{output}\n"
    return ''


def get_listening_ports():
    ports = {}
    output = []
    try:
        nc = psutil.net_connections('inet')
        for c in nc:
            if c.status not in (psutil.CONN_LISTEN, psutil.CONN_NONE):
                continue
            if c.type == socket.SOCK_STREAM:
                proto = 'tcp'
            elif c.type == socket.SOCK_DGRAM:
                proto = 'udp'
            else:
                continue
            if c.family == socket.AF_INET:
                proto += '4'
            else:
                proto += '6'
            ip, port = c.laddr
            ports[f'{ip}#{port}#{proto}'] = {
                'proto': proto,
                'ip': f'[{ip}]'
                if c.family == socket.AF_INET6
                and not ip.startswith('[')
                and not ip.endswith(']')
                else ip,
                'port': port,
            }
        for _, value in ports.items():
            output.append(value)
        output = sorted(output, key=lambda d: (d['port'], d['proto'], d['ip']))
    except Exception:
        pass
    if output:
        msg = 'Listening TCP/UDP Ports (ordered by port, proto, ip):\n'
        for p in output:
            msg += f'* {p["ip"]}:{p["port"]}/{p["proto"]}\n'
        msg += '\n'
        return msg
    return ''


def get_nondefault_software():
    success, result = lib.shell.shell_exec(
        'dnf repoquery --userinstalled --queryformat "%{name};%{version};%{from_repo};%{installtime}"'
    )
    if not success:
        return ''
    stdout, _, _ = result
    table_data = []
    header = ['name', 'version', 'from_repo', 'installtime']
    for line in stdout.splitlines():
        data = dict(zip(header, line.split(';')))
        table_data.append(data)
    output = lib.base.get_table(
        table_data,
        header,
        header=header,
    )
    if output:
        return f'Non-default Software (ordered by name):\n{output}\n'
    return ''


def get_nondefault_users():
    default_linux_users = [
        '_apt',
        '_rpc',
        'abrt',
        'adm',
        'avahi',
        'backup',
        'bin',
        'chrony',
        'colord',
        'daemon',
        'dbus',
        'ftp',
        'games',
        'gnats',
        'halt',
        'irc',
        'list',
        'lp',
        'mail',
        'man',
        'messagebus',
        'news',
        'nobody',
        'operator',
        'pi',
        'polkitd',
        'proxy',
        'pulse',
        'rngd',
        'root',
        'rpc',
        'rpcuser',
        'shutdown',
        'sshd',
        'sssd',
        'sync',
        'sys',
        'systemd-coredump',
        'systemd-network',
        'systemd-oom',
        'systemd-resolve',
        'systemd-timesync',
        'tcpdump',
        'tss',
        'unbound',
        'user',
        'uucp',
    ]
    passwd = lib.base.coe(lib.disk.read_file('/etc/passwd'))
    table_data = []
    header = ['user', 'pw', 'uid', 'gid', 'comment', 'home_dir', 'user_shell']
    for line in passwd.splitlines():
        data = dict(zip(header, line.split(':')))
        if data['user'] not in default_linux_users:
            table_data.append(data)

    output = lib.base.get_table(
        table_data,
        header,
        header=header,
        sort_by_key='user',
    )
    if output:
        return f'Non-default Users:\n{output}\n'
    return ''


def get_public_ip(args):
    success, pub_ip = lib.net.get_public_ip(
        args.PUBLIC_IP_URL,
        insecure=args.INSECURE,
        no_proxy=args.NO_PROXY,
        timeout=args.TIMEOUT,
    )
    if success:
        return f'Public IP {pub_ip}, '
    return ''


def get_software_found():
    """Manually installed software, found on the system"""
    guessed = [
        {
            'sw': 'Apache Solr',
            'expr': [os.path.isdir('/opt/apache-solr'), os.path.isdir('/opt/solr')],
        },
        {
            'sw': 'Apache Tomcat',
            'expr': [os.path.isdir('/opt/apache-tomcat'), os.path.isdir('/opt/tomcat')],
        },
        {
            'sw': 'Atlassian Bitbucket',
            'expr': [os.path.isdir('/opt/atlassian/bitbucket')],
        },
        {
            'sw': 'Atlassian Confluence',
            'expr': [os.path.isdir('/opt/atlassian/confluence')],
        },
        {'sw': 'Atlassian Jira', 'expr': [os.path.isdir('/opt/atlassian/jira')]},
        {'sw': 'Atomicorp', 'expr': [os.path.isdir('/opt/atomicorp')]},
        {'sw': 'Bacchus', 'expr': [os.path.isdir('/opt/bacchus')]},
        {'sw': 'Contao', 'expr': [os.path.isdir('/var/www/html/contao')]},
        {'sw': 'DCM4CHEE', 'expr': [os.path.isdir('/opt/dcm4chee')]},
        {'sw': 'Django', 'expr': [os.path.isdir('/opt/django')]},
        {
            'sw': 'Grav',
            'expr': [
                os.path.isdir('/var/www/html/grav'),
                os.path.isdir('/var/www/grav'),
            ],
        },
        {
            'sw': 'H-Net eFaktura',
            'expr': [os.path.isdir('/home/hnet/HnetSecureService')],
        },
        {'sw': 'Hostbill', 'expr': [os.path.isdir('/home/hostbill')]},
        {'sw': 'HTMLy', 'expr': [os.path.isdir('/var/www/html/htmly')]},
        {'sw': 'JBoss', 'expr': [os.path.isdir('/opt/jboss')]},
        {'sw': 'JumpCloud Agent', 'expr': [os.path.isdir('/opt/jc')]},
        {'sw': 'KeeWeb', 'expr': [os.path.isdir('/opt/KeeWeb')]},
        {
            'sw': 'Keycloak',
            'expr': [
                os.path.isdir('/opt/keycloak'),
                os.path.isdir('/var/log/keycloak'),
            ],
        },
        {'sw': 'LibreNMS', 'expr': [os.path.isdir('/opt/librenms')]},
        {
            'sw': 'MariaDB ColumnStore',
            'expr': [os.path.isdir('/usr/local/mariadb/columnstore')],
        },
        {
            'sw': 'Matomo',
            'expr': [
                os.path.isdir('/var/www/matomo'),
                os.path.isdir('/var/www/html/matomo'),
                os.path.isdir('/var/www/html/piwik'),
            ],
        },
        {
            'sw': 'MediaWiki',
            'expr': [
                os.path.isdir('/var/www/mediawiki'),
                os.path.isdir('/var/www/html/mediawiki'),
            ],
        },
        {'sw': 'Medidata (eFaktura)', 'expr': [os.path.isdir('/opt/MPCommunicator')]},
        {'sw': 'Metabase', 'expr': [os.path.isdir('/opt/metabase')]},
        {
            'sw': 'Nextcloud',
            'expr': [
                os.path.isdir('/var/www/html/nextcloud'),
                os.path.isdir('/var/www/nextcloud'),
                os.path.isfile('/var/www/html/nextcloud/occ'),
            ],
        },
        {'sw': 'NodeBB', 'expr': [os.path.isdir('/opt/nodebb')]},
        {'sw': 'OnlyOffice', 'expr': [os.path.isdir('/var/log/onlyoffice')]},
        {
            'sw': 'ownCloud',
            'expr': [
                os.path.isdir('/var/www/owncloud'),
                os.path.isdir('/var/www/html/owncloud'),
            ],
        },
        {'sw': 'Rambox', 'expr': [os.path.isdir('/opt/Rambox')]},
        {
            'sw': 'Rocket.Chat',
            'expr': [
                os.path.isdir('/opt/Rocket.Chat'),
                os.path.isdir('/opt/rocket.chat'),
            ],
        },
        {'sw': 'Roundcube', 'expr': [os.path.isdir('/var/www/html/roundcubemail')]},
        {'sw': 'Tarifpool v2', 'expr': [os.path.isdir('/opt/tarifpool')]},
        {'sw': 'VMware Tools', 'expr': [os.path.isdir('/etc/vmware-tools')]},
        {'sw': 'Vtiger', 'expr': [os.path.isdir('/var/www/html/vtigercrm')]},
        {'sw': 'WHMCS', 'expr': [os.path.isdir('/home/whmcs')]},
        {'sw': 'Wildfly', 'expr': [os.path.isdir('/opt/wildfly')]},
        {
            'sw': 'Wordpress',
            'expr': [
                os.path.isdir('/var/www/html/wordpress'),
                os.path.isdir('/var/www/html/wp-config.php'),
                os.path.isdir('/var/www/wordpress'),
            ],
        },
        {
            'sw': 'Yii2',
            'expr': [
                os.path.isdir('/var/www/html/yii2'),
                os.path.isdir('/var/www/html/yii2-advanced'),
                os.path.isdir('/var/www/html/yii2-basic'),
                os.path.isdir('/var/www/html/yii'),
            ],
        },
        {'sw': 'Zimbra', 'expr': [os.path.isdir('/opt/zimbra')]},
    ]
    output = ''
    for item in guessed:
        # if any of the listed software packages is installed, we have a match
        if any(item['expr']):
            output += f'* {item["sw"]}\n'
    if output:
        return f'Software found elsewhere (just guessed):\n{output}\n'
    return ''


def get_sys_dimensions():
    """get some very basic system statistics"""
    sys_dimensions = {}
    try:
        sys_dimensions['cpu_logical'] = psutil.cpu_count(logical=True)
        sys_dimensions['cpu_physical'] = psutil.cpu_count(logical=False)
        sys_dimensions['cpu_usable'] = psutil.cpu_count(logical=True)
        sys_dimensions['cpu_freq'] = psutil.cpu_freq()
        sys_dimensions['ram'] = psutil.virtual_memory().total
    except Exception:
        pass
    return sys_dimensions


def get_systemd_default_target():
    cmd = 'systemctl get-default'
    success, result = lib.shell.shell_exec(cmd)
    if not success:
        return ''
    stdout, stderr, retc = result
    if stderr or retc != 0:
        return ''
    return f'systemctl get-default:\n* {stdout.strip()}\n\n'


def get_systemd_timers():
    # using `--output=json` sadly does not work with older systemd versions
    # (eg systemd 219 on CentOS 7 or systemd 239 on RHEL 8), therefore we have to parse
    # the human output.
    # in order to list for a different user (`--user`), we would need to sudo to that user
    # first - we will skip that for now
    success, result = lib.shell.shell_exec('systemctl list-timers')
    if not success:
        return ''
    stdout, stderr, retc = result
    if stderr or retc != 0:
        return ''

    table_data = []
    next_pos = None
    left_pos = None
    unit_pos = None
    activates_pos = None
    for line in stdout.splitlines():
        if next_pos is None:
            # clutters a little bit on modern systemd (Fedora) because of right-aligned LEFT
            next_pos = line.find('NEXT')
            left_pos = line.find('LEFT')
            unit_pos = line.find('UNIT')
            activates_pos = line.find('ACTIVATES')
        if '.timer' in line:
            table_data.append(
                {
                    'unit': line[unit_pos:activates_pos].strip(),
                    'activates': line[activates_pos:].strip(),
                    'next': line[next_pos:left_pos].strip(),
                }
            )

    output = lib.base.get_table(
        table_data,
        ['unit', 'activates', 'next'],
        header=['unit', 'activates', 'next'],
        sort_by_key='unit',
    )
    if output:
        return f'systemctl list-timers:\n{output}\n'
    return ''


def get_systemd_units(cmd):
    # using `--output=json` sadly does not work with older systemd versions
    # (eg systemd 219 on CentOS 7 or systemd 239 on RHEL 8), therefore we have to parse
    # the human output.
    # in order to list for a different user (`--user`), we would need to sudo to that user
    # first - we will skip that for now
    success, result = lib.shell.shell_exec(cmd)
    if not success:
        return ''
    stdout, stderr, retc = result
    if stderr or retc != 0:
        return ''
    if not stdout:
        return ''

    output = f'{cmd.replace(" --no-legend", "")}:\n'
    for line in stdout.splitlines():
        output += f'* {line.split()[0]}\n'
    return output + '\n'


def get_tuned_active_profile():
    """Return current active tuned profile (if any)."""
    output = run_cmd('tuned-adm active')
    if output:
        return (
            f'tuned profile "{run_regex(output, ": (.*)").strip().replace("* ", "")}", '
        )
    return ''


def get_virt_info():
    # alternative would be `/usr/sbin/virt-what` (POSIX shell script)
    success, result = lib.shell.shell_exec('systemd-detect-virt')
    if success:
        stdout, _, _ = result
        return stdout.strip()
    return 'Unknown'


def run_cmd(cmd, shell=False, ignore_output=False):
    """Run a command and return its output. Returns stderr if cmd prints its standard output there.
    If ignore_output is set to True, returns True. Returns False if cmd is not found.
    """
    env = os.environ.copy()
    env['LC_ALL'] = 'C'
    env['PATH'] += ':/usr/local/bin:/usr/local/sbin'
    # shell is opt-in per call site and all call sites pass hardcoded commands
    success, result = lib.shell.shell_exec(cmd, shell=shell, env=env)  # nosec B604
    if not success:
        return False
    stdout, stderr, retc = result
    if retc != 0:
        # for example if using `command -v loolwsd`
        return False
    if ignore_output:
        return True
    if stdout == '' and stderr != '':
        # https://stackoverflow.com/questions/26028416/why-does-python-print-version-info-to-stderr
        # https://stackoverflow.com/questions/13483443/why-does-java-version-go-to-stderr
        stdout = stderr
    try:
        return stdout.strip().splitlines()[0].strip()
    except IndexError:
        return True


def run_regex(haystack, regex, group=1):
    """Apply a regex to a haystack, assume first match group, otherwise let us choose which one."""
    re_search = re.search(regex, haystack)
    if re_search:
        return re_search.group(group).strip()
    return ''


def main():
    """The main function. This is where the magic happens."""

    # parse the command line
    try:
        args = parse_args()
    except SystemExit:
        sys.exit(STATE_UNKNOWN)

    # init some vars
    distro = lib.distro.get_distribution_facts()
    perfdata = ''
    tags = get_lftags(distro)

    # only tags wanted, so it's ok to stop here
    if args.TAGS:
        lib.base.oao(tags, STATE_OK)

    # build the message

    # first header line ----------------------------------------------------------------------------
    msg = f'{socket.gethostname()}: '

    os_info = distro.get('os_info')
    if os_info:
        msg += f'{os_info} '
    msg += f'Kernel {run_cmd("uname -r")} '

    virt = get_virt_info()
    if virt == 'none':
        msg += 'on Bare-Metal, '
    else:
        msg += f'virtualized on {virt}, '

    success, firmware_device_model = lib.disk.read_file(
        '/sys/firmware/devicetree/base/model'
    )
    if not success:
        firmware_device_model = ''
    if firmware_device_model:
        msg += f'{firmware_device_model}, '

    dmi = lib.dmidecode.get_data() if args.DMIDECODE else False
    sys_dimensions = get_sys_dimensions()
    if dmi and sys_dimensions:
        # combine the best from both worlds
        msg += f'{lib.dmidecode.manufacturer(dmi)} {lib.dmidecode.model(dmi)}, '
        msg += f'Firmware: {lib.dmidecode.firmware(dmi)}, '
        msg += f'SerNo: {lib.dmidecode.serno(dmi)}, '
        msg += f'Proc: {lib.dmidecode.cpu_type(dmi)}, '
        msg += (
            f'{sys_dimensions["cpu_physical"]}'
            f'/{sys_dimensions["cpu_logical"]}'
            f'/{sys_dimensions["cpu_usable"]}'
            f' CPUs (phys/lcpu/onln), '
        )
        if sys_dimensions['cpu_freq']:
            msg += f'Current Speed: {int(sys_dimensions["cpu_freq"][0])} MHz, '
        msg += (
            f'{lib.human.bytes2human(sys_dimensions["ram"])}'
            f'/{lib.human.bytes2human(lib.dmidecode.ram(dmi))}'
            f' RAM (virtmem/max'
        )
        if sys_dimensions['ram'] > lib.dmidecode.ram(dmi):
            msg += '; reboot recommended'
        msg += '), '
    elif dmi:
        msg += f'{lib.dmidecode.manufacturer(dmi)} {lib.dmidecode.model(dmi)}, '
        msg += f'Firmware: {lib.dmidecode.firmware(dmi)}, '
        msg += f'SerNo: {lib.dmidecode.serno(dmi)}, '
        msg += f'Proc: {lib.dmidecode.cpu_type(dmi)}, '
        msg += f'{lib.human.bytes2human(lib.dmidecode.ram(dmi))} RAM, '
    elif sys_dimensions:
        msg += (
            f'{sys_dimensions["cpu_physical"]}'
            f'/{sys_dimensions["cpu_logical"]}'
            f'/{sys_dimensions["cpu_usable"]}'
            f' CPUs (phys/lcpu/onln), '
        )
        if sys_dimensions['cpu_freq']:
            msg += f'Current Speed: {int(sys_dimensions["cpu_freq"][0])} MHz, '
        msg += f'{lib.human.bytes2human(sys_dimensions["ram"])} RAM, '
    else:
        msg += 'sys dimensions n/a (consider installing dmidecode/psutil), '

    disk_msg, disk_count = get_disks()
    msg += disk_msg
    msg += get_boot_mode()
    env = os.environ.copy()
    display_server = env.get('XDG_SESSION_TYPE')
    if display_server and display_server != 'unspecified':
        msg += f'Display Server {display_server}, '
    msg += get_tuned_active_profile()
    msg += get_public_ip(args)
    msg += get_birthday()
    msg += f'About-me v{__version__}\n\n'

    # multi-line content - print further lines -----------------------------------------------------
    if args.DMIDECODE:
        msg += get_hw_info()
    msg += get_interfaces()
    msg += get_listening_ports()
    msg += get_nondefault_software()
    msg += get_software_found()
    msg += get_nondefault_users()
    msg += get_systemd_default_target()
    msg += get_systemd_units(
        'systemctl list-unit-files --type=service --state=enabled --no-legend',
    )
    msg += get_systemd_units(
        'systemctl list-unit-files --type=mount --state=static --state=generated --no-legend',
    )
    msg += get_systemd_units(
        'systemctl list-unit-files --type=automount --state=enabled --state=static --no-legend',
    )
    msg += get_systemd_timers()
    msg += get_crontab()
    msg += tags

    # perfdata
    if os_info:
        re_search = re.search(r'[\d\.]+', os_info)
        if re_search:
            perfdata += lib.base.get_perfdata(
                'osversion',
                re_search.group(0).replace('.', ''),
            )
    if sys_dimensions:
        perfdata += lib.base.get_perfdata(
            'cpu_logical',
            sys_dimensions['cpu_logical'],
            _min=0,
        )
        perfdata += lib.base.get_perfdata(
            'cpu_physical',
            sys_dimensions['cpu_physical'],
            _min=0,
        )
        perfdata += lib.base.get_perfdata(
            'cpu_usable',
            sys_dimensions['cpu_usable'],
            _min=0,
        )
        if sys_dimensions['cpu_freq']:
            perfdata += lib.base.get_perfdata(
                'cpu_freq',
                int(sys_dimensions['cpu_freq'][0]),
                _min=0,
                _max=lib.dmidecode.cpu_speed(dmi) if args.DMIDECODE and dmi else None,
            )
    if dmi:
        perfdata += lib.base.get_perfdata(
            'ram',
            lib.dmidecode.ram(dmi),
            uom='B',
            _min=0,
        )
    elif sys_dimensions:
        perfdata += lib.base.get_perfdata(
            'ram',
            sys_dimensions['ram'],
            uom='B',
            _min=0,
        )
    perfdata += lib.base.get_perfdata(
        'disks',
        disk_count,
        _min=0,
    )

    # over and out
    lib.base.oao(msg, STATE_OK, perfdata)


if __name__ == '__main__':
    try:
        main()
    except Exception:
        lib.base.cu()
