#!/usr/bin/python3
"""Manage support files for Pacemaker CTS."""

# pylint doesn't like the module name "cts-attrd" which is an invalid complaint for this file
# but probably something we want to continue warning about elsewhere
# pylint: disable=invalid-name
# pacemaker imports need to come after we modify sys.path, which pylint will complain about.
# pylint: disable=wrong-import-position
# We access various private members several places in this file, so disable this warning
# file-wide.
# pylint: disable=protected-access

__copyright__ = "Copyright 2024 the Pacemaker project contributors"
__license__ = "GNU General Public License version 2 or later (GPLv2+) WITHOUT ANY WARRANTY"

import argparse
import fcntl
import os
import shutil
import subprocess
import sys

# These imports allow running from a source checkout after running `make`.
# Note that while this doesn't necessarily mean it will successfully run tests,
# but being able to see --help output can be useful.
if os.path.exists("/builddir/build/BUILD/pacemaker-9a5e54bae/python"):
    sys.path.insert(0, "/builddir/build/BUILD/pacemaker-9a5e54bae/python")

# pylint: disable=comparison-of-constants,comparison-with-itself,condition-evals-to-constant
if os.path.exists("/builddir/build/BUILD/pacemaker-9a5e54bae/python") and "/builddir/build/BUILD/pacemaker-9a5e54bae" != "/builddir/build/BUILD/pacemaker-9a5e54bae":
    sys.path.insert(0, "/builddir/build/BUILD/pacemaker-9a5e54bae/python")

from pacemaker.buildoptions import BuildOptions
from pacemaker.exitstatus import ExitStatus

COROSYNC_RUNTIME_CONF = "cts.conf"
COROSYNC_RUNTIME_UNIT = "corosync.service.d"
DUMMY_DAEMON = "pacemaker-cts-dummyd"
DUMMY_DAEMON_UNIT = "pacemaker-cts-dummyd@.service"

FENCE_DUMMY = "fence_dummy"
FENCE_DUMMY_ALIASES = ["auto_unfence", "no_reboot", "no_on", "no_nodeid"]
LSB_DUMMY = "LSBDummy"


def daemon_reload():
    """Reload the systemd daemon."""
    try:
        subprocess.call(["systemctl", "daemon-reload"])
    except subprocess.SubprocessError:
        pass


def install(src, destdir, mode=0o755):
    """Install a file to a given directory with the given mode."""
    destfile = "%s/%s" % (destdir, os.path.basename(src))

    shutil.copyfile(src, destfile)
    os.chmod(destfile, mode)


def makedirs_if_missing(path):
    """If the directory path doesn't exist, create it."""
    if os.path.exists(path):
        return

    os.makedirs(path)


def cmd_install(src):
    """Install support files needed by Pacemaker CTS."""
    cmd_uninstall()

    if not os.path.exists(src):
        sys.exit(ExitStatus.ERROR)

    os.chdir(src)

    if os.path.exists(BuildOptions.UNIT_DIR):
        print("Installing %s ..." % DUMMY_DAEMON)
        d = "%s/pacemaker" % BuildOptions.LIBEXEC_DIR

        makedirs_if_missing(d)
        install(DUMMY_DAEMON, d)

        print("Installing %s ..." % DUMMY_DAEMON_UNIT)
        install(DUMMY_DAEMON_UNIT, BuildOptions.UNIT_DIR)
        daemon_reload()

    runtime_unit_dir = "%s/systemd/system" % BuildOptions.RUNTIME_STATE_DIR
    if os.path.exists(runtime_unit_dir):
        unit_dir = "%s/%s" % (runtime_unit_dir, COROSYNC_RUNTIME_UNIT)

        print("Installing %s to %s ..." % (COROSYNC_RUNTIME_CONF, unit_dir))
        makedirs_if_missing(unit_dir)
        install(COROSYNC_RUNTIME_CONF, unit_dir, 0o644)
        daemon_reload()

    print("Installing %s to %s ..." % (FENCE_DUMMY, BuildOptions._FENCE_BINDIR))
    makedirs_if_missing(BuildOptions._FENCE_BINDIR)
    install(FENCE_DUMMY, BuildOptions._FENCE_BINDIR)

    for alias in FENCE_DUMMY_ALIASES:
        print("Installing fence_dummy_%s to %s ..." % (alias, BuildOptions._FENCE_BINDIR))
        try:
            os.symlink(FENCE_DUMMY, "%s/fence_dummy_%s" % (BuildOptions._FENCE_BINDIR, alias))
        except OSError:
            sys.exit(ExitStatus.ERROR)

    if BuildOptions.INIT_DIR is not None:
        print("Installing %s to %s ..." % (LSB_DUMMY, BuildOptions.INIT_DIR))
        makedirs_if_missing(BuildOptions.INIT_DIR)
        install(LSB_DUMMY, BuildOptions.INIT_DIR)


def cmd_uninstall():
    """Remove support files needed by Pacemaker CTS."""
    dummy_unit_file = "%s/%s" % (BuildOptions.UNIT_DIR, DUMMY_DAEMON_UNIT)

    if os.path.exists(dummy_unit_file):
        print("Removing %s ..." % dummy_unit_file)
        os.remove(dummy_unit_file)
        daemon_reload()

    corosync_runtime_dir = "%s/systemd/system/%s" % (BuildOptions.RUNTIME_STATE_DIR, COROSYNC_RUNTIME_UNIT)

    if os.path.exists(corosync_runtime_dir):
        print("Removing %s ..." % corosync_runtime_dir)
        shutil.rmtree(corosync_runtime_dir)
        daemon_reload()

    for f in ["%s/pacemaker/%s" % (BuildOptions.LIBEXEC_DIR, DUMMY_DAEMON),
              "%s/%s" % (BuildOptions._FENCE_BINDIR, FENCE_DUMMY),
              "%s/%s" % (BuildOptions.INIT_DIR, LSB_DUMMY)]:
        if not os.path.exists(f):
            continue

        print("Removing %s ..." % f)
        os.remove(f)

    for alias in FENCE_DUMMY_ALIASES:
        f = "%s/fence_dummy_%s" % (BuildOptions._FENCE_BINDIR, alias)

        if not os.path.exists(f) and not os.path.islink(f):
            continue

        print("Removing %s ..." % f)
        os.remove(f)


def cmd_watch(filename, limit, offset, prefix):
    """Watch a log file."""
    if not os.access(filename, os.R_OK):
        print("%sLast read: %d, limit=%d, count=%d - unreadable" % (prefix, 0, limit, 0))
        sys.exit(ExitStatus.ERROR)

    with open(filename, "r", encoding="utf-8") as logfile:
        logfile.seek(0, os.SEEK_END)
        newsize = logfile.tell()

        if offset != 'EOF':
            offset = int(offset)

            if newsize >= offset:
                logfile.seek(offset)
            else:
                print("%sFile truncated from %d to %d" % (prefix, offset, newsize))

                if (newsize * 1.05) < offset:
                    logfile.seek(0)

        # Don't block when we reach EOF
        fcntl.fcntl(logfile.fileno(), fcntl.F_SETFL, os.O_NONBLOCK)

        count = 0
        while True:
            if logfile.tell() >= newsize:
                break

            if limit and count >= limit:
                break

            line = logfile.readline()
            if not line:
                break

            print(line.strip())
            count += 1

        print("%sLast read: %d, limit=%d, count=%d" % (prefix, logfile.tell(), limit, count))


def build_options():
    """Handle command line arguments."""
    # Create the top-level parser
    parser = argparse.ArgumentParser(description="Support tool for CTS")
    subparsers = parser.add_subparsers(dest="subparser_name")

    # Create the parser for the "install" command
    subparsers.add_parser("install", help="Install support files")

    # Create the parser for the "uninstall" command
    subparsers.add_parser("uninstall", help="Remove support files")

    # Create the parser for the "watch" command
    watch_parser = subparsers.add_parser("watch", help="Remote log watcher")
    watch_parser.add_argument("-f", "--filename", default="/var/log/messages",
                              help="File to watch")
    watch_parser.add_argument("-l", "--limit", type=int, default=0,
                              help="Maximum number of lines to read")
    watch_parser.add_argument("-o", "--offset", default=0,
                              help="Which line number to start reading from")
    watch_parser.add_argument("-p", "--prefix", default="",
                              help="String to add to the beginning of each line")

    args = parser.parse_args()
    return args


if __name__ == "__main__":
    opts = build_options()

    if os.geteuid() != 0:
        print("This command must be run as root")
        sys.exit(ExitStatus.ERROR)

    # If the install directory doesn't exist, assume we're in a build directory.
    data_dir = "%s/pacemaker/tests/cts" % BuildOptions.DATA_DIR
    if not os.path.exists(data_dir):
        data_dir = "%s/pacemaker/tests/cts" % BuildOptions._BUILD_DIR

    if opts.subparser_name == "install":
        cmd_install(data_dir)

    if opts.subparser_name == "uninstall":
        cmd_uninstall()

    if opts.subparser_name == "watch":
        cmd_watch(opts.filename, opts.limit, opts.offset, opts.prefix)

# vim: set filetype=python expandtab tabstop=4 softtabstop=4 shiftwidth=4 textwidth=120:
