#!/usr/bin/env python3

# Copyright (c) 2022 SUSE LLC  All rights reserved.
#
# check_rmt_repos 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, version 2 of
# the License.
#
# check_rmt_repos 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 check_rmt_repos. If not, see
# <http://www.gnu.org/licenses/>.
#

import argparse
import csv
import json
import os
import subprocess
import sys
import yaml

from io import StringIO

# Nagios states
OK = 0
WARNING = 1
CRITICAL = 2
UNKNOWN = 3


def get_enabled_rmt_repos():
    repo_info_cmd = ['rmt-cli', 'repos', 'list', '--csv']
    proc = subprocess.Popen(
        repo_info_cmd,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE
    )
    avail_repo_data, errors = proc.communicate()

    if errors or proc.returncode != 0:
        print('Error executing "rmt-cli repos list"')
        sys.exit(CRITICAL)

    rmt_repos = {}
    f = StringIO(avail_repo_data.decode())
    f.readline()  # skip table headers
    csv_reader = csv.reader(f, delimiter=',')
    for repo_detail in csv_reader:
        id = repo_detail[0].strip()
        description = repo_detail[2].strip()
        rmt_repos[id] = description

    return rmt_repos


def get_pass_through_repos():
    """RMT has a configuration option to allow repositories to be set up
       as pass through, meaning the content of those repositories does not
       get coppied onto the RMT server. We need to skip these repos when
       checking for precense of content on the server."""
    rmt_config_path = '/etc/rmt.conf'
    pass_through_repos = []
    if not os.path.exists(rmt_config_path):
        return pass_through_repos
    rmt_config = {}
    try:
        with open(rmt_config_path, 'r') as f:
            rmt_config = yaml.safe_load(f)
    except:
        return pass_through_repos
    mirror_config = rmt_config.get('mirroring')
    if mirror_config:
        pass_through_repos = mirror_config.get('redirect_repo_hosts', [])

    return pass_through_repos
    
        
def get_rmt_cli_command():
    rmt_cli_pid = get_rmt_cli_pid()
    rmt_cli_cmd = None
    if rmt_cli_pid:
        rmt_cli_cmd = open('/proc/%s/cmdline' % rmt_cli_pid, 'r').read()
        rmt_cli_cmd = rmt_cli_cmd.replace('\x00', ' ')
    return rmt_cli_cmd


def get_rmt_cli_pid():
    rmt_cli_pid_cmd = ['ps', '-C', 'rmt-cli', '-o', 'pid=']
    rmt_cli_pid = subprocess.Popen(rmt_cli_pid_cmd, stdout=subprocess.PIPE)
    pid_data = rmt_cli_pid.communicate()
    return pid_data[0].strip().decode()


def get_mirrored_repos():
    mirrored_repos = {}
    for root, dirs, files in os.walk('/var/lib/rmt/public/repo'):
        for dirname in dirs:
            if dirname == 'repodata':
                mirrored_repos[root] = True
    return mirrored_repos


def update_prometheus_data(metrics):
    script = os.path.splitext(
        os.path.basename(node_exporter_path)
    )[0]
    info = ''
    with open(node_exporter_path, 'w') as prom_file:
        for key, metric in metrics.items():
            info = info + \
                '{script} {{status="{status}",repos="{repos}"}} {n_repos}\n'.format(
                    script=script,
                    status=key,
                    n_repos=metric.get('amount'),
                    repos=', '.join(metric.get('repos'))
                )

        prom_file.write(info)


# Set up command line argument parsing
argparse = argparse.ArgumentParser(description='Monitor repo setup for RMT')
argparse.add_argument(
    '-f', '--config-file',
    dest='config_file_path',
    default='/etc/rmt-utils/rmt_repository_config.json',
    help='Path to json config file for RMT repo configuration',
    metavar='CONFIG_FILE'
)
help_msg = 'A designator that will compare the repository configuration '
help_msg += '"framework" setting with the provided value'
argparse.add_argument(
    '--cloud',
    dest='framework',
    help=help_msg
)
args = argparse.parse_args()
node_exporter_path = '/var/lib/node_exporter/textfile_collector/check_rmt_repos.prom'
if not os.path.exists(os.path.dirname(node_exporter_path)):
    try:
        os.mkdir(os.path.dirname(node_exporter_path))
    except OSError:
        print('ERROR creating {}'.format(os.path.dirname(node_exporter_path)))


enabled_repos = get_enabled_rmt_repos()
mirrored_repos = get_mirrored_repos()

if not os.path.exists(args.config_file_path):
    print('Missing "%s"' % args.config_file_path)
    sys.exit(CRITICAL)

rmt_cli_command = get_rmt_cli_command()
if rmt_cli_command and 'mirror' in rmt_cli_command:
    # While the mirror process is running we do not complain about
    # missing directories. They are expected to show up at the end
    # of the mirror process.
    sys.exit(OK)

framework_to_use = None
if args.framework:
    framework_to_use = args.framework

configured_repo_ids = []
not_enabled = {'ids': [], 'descriptions': []}
missing_repo_dirs = []
extra_enabled_repos = {'ids': [], 'descriptions': []}
extra_mirrored_repos = []
config_repos = []
try:
    json_file = open(args.config_file_path)
    config_repos = json.load(json_file)
except:
    print('Could not open or read "%s"' % args.config_file_path)
    sys.exit(CRITICAL)

pass_through_domains = get_pass_through_repos()
for config_repo in config_repos:
    repo_id = str(config_repo.get('id'))
    repo_framework = config_repo.get('framework')
    if repo_framework and repo_framework != framework_to_use:
        # Skip repos that are not configured for this framework
        continue
    configured_repo_ids.append(repo_id)
    if not enabled_repos.get(repo_id):
        not_enabled['ids'].append(repo_id)
        not_enabled['descriptions'].append(
            config_repo.get('description')
        )
    full_repo_path = '/var/lib/rmt/public/repo/' \
        + config_repo.get('location').strip('/')

    if mirrored_repos.get(full_repo_path):
        del mirrored_repos[full_repo_path]

    for domain in pass_through_domains:
        if domain in config_repo.get('url', ''):
            break
    else:
        if not os.path.exists(full_repo_path):
            missing_repo_dirs.append(full_repo_path)

for repo_id, repo_description in enabled_repos.items():
    if repo_id not in configured_repo_ids:
        extra_enabled_repos['ids'].append(repo_id)
        extra_enabled_repos['descriptions'].append(repo_description)
extra_mirrored_repos = list(mirrored_repos.keys())

metrics = {
    'not_enabled': {
        'amount': len(not_enabled.get('ids')),
        'repos': not_enabled.get('ids')
    },
    'missing': {
        'amount': len(missing_repo_dirs),
        'repos': missing_repo_dirs
    },
    'extra_enabled': {
        'amount': len(extra_enabled_repos.get('ids')),
        'repos': extra_enabled_repos.get('ids')
    },
    'extra_mirrored': {
        'amount': len(extra_mirrored_repos),
        'repos': extra_mirrored_repos
    }
}
update_prometheus_data(metrics)


if not_enabled['ids']:
    print('Following expected repos are not enabled:')
    print('\t%s' % ' '.join(not_enabled['ids']))
    for description in not_enabled['descriptions']:
        print('\t%s' % description)

if missing_repo_dirs:
    print('Following repo directories are missing:')
    for missing_dir in missing_repo_dirs:
        print('\t%s' % missing_dir)

if extra_enabled_repos['ids']:
    print('Following enabled repos are extra:')
    print('\t%s' % ' '.join(extra_enabled_repos['ids']))
    for description in extra_enabled_repos['descriptions']:
        print('\t%s' % description)

if extra_mirrored_repos:
    print('Following mirrored repos are extra:')
    for extra_dir in extra_mirrored_repos:
        print('\t%s' % extra_dir)

if (
        not_enabled['ids'] or
        missing_repo_dirs or
        extra_enabled_repos['ids'] or
        extra_mirrored_repos
):
    sys.exit(CRITICAL)
