#!/usr/bin/python3 -tt
# - *- coding: utf- 8 - *-
#
#
# Description:  Manages a single sapstartrv on an SAP Instance as
#               a High-Availability resource.
#
# Author:       Xabier Arbulu, September 2020
# Based on code from: Fabian Herschel
# Support:
# License:      GNU General Public License (GPL)
# Copyright:    (c) 2020-2023 SUSE LLC
#
# An example usage:
#      See usage() function below for more details...
#
# OCF instance parameters:
#       - OCF_RESKEY_InstanceName
#       - not currently OCF_RESKEY_START_PROFILE (optional, well known
#         directories will be searched by default)
#
#   - supports sapstartsrv for SAP instances NW7.40 or newer,
#     SAP S/4HANA ABAP Platform 1909 or newer
#     (central services and enqueue replication)
#   - MUST NOT be used for SAP HANA in system replication
#   - MUST NOT be used for SAP HANA standalone Scale-Up or Scale-Out systems
#
#######################################################################

import os
import sys
import re
import subprocess
import shlex
import psutil

OCF_FUNCTIONS_DIR = os.environ.get(
    "OCF_FUNCTIONS_DIR",
    "%s/lib/heartbeat" % os.environ.get("OCF_ROOT"))
sys.path.append(OCF_FUNCTIONS_DIR)

import ocf  # noqa: E402
from ocf import logger  # noqa: E402

LONG_DESC = '''Long description of SAPStartSrv to be done (python version)'''

SHORT_DESC = 'Manages an sapstartsrv of a specific SAP instance as an HA resource (python version)'

MONITOR_SERVICES_DEFAULT = \
    'disp+work|msg_server|enserver|enrepserver|jcontrol|jstart|enq_server|enq_replicator'

SYSTEMCTL = '/usr/bin/systemctl'


class ProcessResult(object):
    """
    Class to store subprocess.Popen output information and offer some
    functionalities

    Args:
        cmd (str): Executed command
        returncode (int): Subprocess return code
        output (str): Subprocess output string
        err (str): Subprocess error string
    """

    def __init__(self, cmd, returncode, output, err):
        self.cmd = cmd
        self.returncode = returncode
        self.output = output.decode()  # Make it compatiable with python2 and 3
        self.err = err.decode()


def run_command(cmd):
    '''
    Run shell command and return result and outputs
    '''
    proc = subprocess.Popen(
        shlex.split(cmd),
        stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE)

    out, err = proc.communicate()

    return ProcessResult(cmd, proc.returncode, out, err)


class SapStartSrv(object):
    '''
    SapStartSrv class
    '''

    def __init__(self, instance_name):
        self.full_name = instance_name
        self.sid = None
        self.sidadm = None
        self.instance_name = None
        self.instance_number = None
        self.systemd_unit_name = None
        self.virtual_host = None
        self.dir_executable = None
        self.saptstartsrv_path = None
        self.sapcontrol_path = None
        self.start_profile = None
        self.sap_instance_profile = None

    def _get_status(self):
        '''
        Get sapstartsrv status returning ProcessResult object
        '''
        sap_inst_pfpat = r'pf=.*/{}_{}_{}'.format(self.sid, self.instance_name, self.virtual_host)
        sap_inst_regex = re.compile(sap_inst_pfpat)
        result = 1
        res_out = 'No running sapstartsrv process found for {} with {}'.format(
            self.saptstartsrv_path, sap_inst_pfpat)
        for proc in psutil.process_iter():
            try:
                process = proc.as_dict(attrs=['pid', 'ppid', 'name', 'cmdline'])
            except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
                continue
            else:
                cmdline = process.get('cmdline', '-')
                pname = process.get('name', '-')
                pid = process.get('pid', '-')
                ppid = process.get('ppid', '-')
                if pname == "sapstartsrv":
                    if any(sap_inst_regex.match(item) for item in cmdline):
                        result = 0
                        res_out = 'Found running sapstartsrv process - PID:{} PPID:{} CMDLINE:{}'.format(
                            pid, ppid, cmdline)
                        break

        logger.info('Current status: %d. Output: %s' % (result, res_out))
        return result

    def _find_executables(self):
        '''
        Find sapstartsrv and sapcontrol executables
        '''

        # Find executables in DIR_EXECUTABLE
        self.dir_executable = ocf.get_parameter("DIR_EXECUTABLE")
        if self.dir_executable:
            self.saptstartsrv_path = '{}/sapstartsrv'.format(self.dir_executable)
            self.sapcontrol_path = '{}/sapcontrol'.format(self.dir_executable)
            if ocf.have_binary(self.saptstartsrv_path) and ocf.have_binary(self.sapcontrol_path):
                return ocf.OCF_SUCCESS

            logger.error(
                'Cannot find sapstartsrv and sapcontrol executable in %s' %
                (self.dir_executable))
            return ocf.OCF_ERR_ARGS

        # Find executables in standard locations. E.g: /usr/sap/HA1/ASCS00/exe
        self.dir_executable = '/usr/sap/{}/{}/exe'.format(self.sid, self.instance_name)
        self.saptstartsrv_path = '/usr/sap/{}/{}/exe/sapstartsrv'.format(
            self.sid, self.instance_name)
        self.sapcontrol_path = '/usr/sap/{}/{}/exe/sapcontrol'.format(
            self.sid, self.instance_name)

        if ocf.have_binary(self.saptstartsrv_path) and ocf.have_binary(self.sapcontrol_path):
            return ocf.OCF_SUCCESS

        logger.error(
            'Cannot find sapstartsrv and sapcontrol executable in %s' %
            (self.dir_executable))

        # Find executables in standard locations. E.g: /usr/sap/HA1/ASCS00/exe/run
        self.dir_executable = '/usr/sap/{}/{}/exe/run'.format(self.sid, self.instance_name)
        self.saptstartsrv_path = '/usr/sap/{}/{}/exe/run/sapstartsrv'.format(
            self.sid, self.instance_name)
        self.sapcontrol_path = '/usr/sap/{}/{}/exe/run/sapcontrol'.format(
            self.sid, self.instance_name)
        if ocf.have_binary(self.saptstartsrv_path) and ocf.have_binary(self.sapcontrol_path):
            return ocf.OCF_SUCCESS

        logger.error(
            'Cannot find sapstartsrv and sapcontrol executable in %s' %
            (self.dir_executable))
        return ocf.OCF_ERR_ARGS

    def _export_variables(self):
        '''
        Export needed variables
        '''

        if not os.environ.get('OCF_RESKEY_START_WAITTIME'):
            os.environ['OCF_RESKEY_START_WAITTIME'] = ocf.get_parameter('START_WAITTIME', '3600')

        if not os.environ.get('OCF_RESKEY_MONITOR_SERVICES'):
            os.environ['OCF_RESKEY_MONITOR_SERVICES'] = ocf.get_parameter(
                'MONITOR_SERVICES', MONITOR_SERVICES_DEFAULT)

        library_path = os.environ.get('LD_LIBRARY_PATH', '')
        if self.dir_executable not in library_path:
            os.environ['LD_LIBRARY_PATH'] = '{}{}{}'.format(
                library_path, ':' if library_path else '', self.dir_executable)

    def _inititialize(self):
        '''
        Initialize variables
        '''

        if len(self.full_name.split('_')) < 3:
            logger.error('InstanceName parsing error. It must follow SID_NAME00_VIRTHOST syntax')
            return ocf.OCF_ERR_ARGS

        try:
            self.sid = self.full_name.split('_')[0]
            self.instance_name = self.full_name.split('_')[1]
            self.virtual_host = '_'.join(self.full_name.split('_')[2:])
            instance_data = re.match('[a-zA-Z]+([0-9]{2})', self.instance_name).groups()
            self.instance_number = instance_data[0]
            self.sidadm = '{}adm'.format(self.sid.lower())
            self.systemd_unit_name = 'SAP{}_{}.service'.format(self.sid.upper(), self.instance_number)
        except (IndexError, AttributeError):
            logger.error('InstanceName parsing error. It must follow SID_NAME00_VIRTHOST syntax')
            return ocf.OCF_ERR_ARGS

        result = self._find_executables()
        if result != ocf.OCF_SUCCESS:
            return result

        dir_profile = ocf.get_parameter(
            'DIR_PROFILE', '/usr/sap/{}/SYS/profile'.format(self.sid))
        self.sap_instance_profile = ocf.get_parameter(
            'SAP_INSTANCE_PROFILE', '{}/{}_{}_{}'.format(
                dir_profile, self.sid, self.instance_name, self.virtual_host))

        self._export_variables()

        return ocf.OCF_SUCCESS

    def _is_unit_active(self):
        '''
        Run systemctl is-active unit_name
        '''
        result = run_command(
            '{} is-active {}'.format(SYSTEMCTL, self.systemd_unit_name))
        return result.returncode == 0

    def _get_systemd_unit(self):
        '''
        Run systemctl list-unit-files unit_name
        '''
        pattern = r'.*\s%s.*' % self.systemd_unit_name
        result = run_command(
            '{} list-unit-files {}'.format(SYSTEMCTL, self.systemd_unit_name))
        if result.returncode == 0 and not re.match(pattern, result.output):
            result.returncode = 1
        return result.returncode == 0

    def _chk_systemd_support(self):
        '''
        Check availability of SAP systemd support
        '''
        if ocf.have_binary(SYSTEMCTL):
            unit_file = '/etc/systemd/system/{}'.format(self.systemd_unit_name)
            if os.path.exists(unit_file):
                return True
            if self._get_systemd_unit():
                return True

        return False

    def _start_systemd_style(self):
        '''
        Run systemctl start unit
        '''
        if self._is_unit_active():
            logger.info(
                'systemd service %s is active' % (self.systemd_unit_name))
            return ocf.OCF_SUCCESS

        logger.warn(
            'systemd service %s is not active, it will be started using systemd' %
            (self.systemd_unit_name))
        result = run_command(
            '{} start {}'.format(SYSTEMCTL, self.systemd_unit_name))
        if result.returncode == 0:
            return ocf.OCF_SUCCESS

        logger.error(
            'error during start of systemd unit %s!' %
            (self.systemd_unit_name))
        if ocf.is_probe():
            return ocf.OCF_NOT_RUNNING

        return ocf.OCF_ERR_GENERIC

    def _start_sys5_style(self):
        '''
        Run sapstartsrv command
        '''
        run_command('rm -f /tmp/.sapstream5{}13'.format(self.instance_number))
        run_command('rm -f /tmp/.sapstream5{}14'.format(self.instance_number))
        start_result = run_command('{} pf={} -D -u {}'.format(
            self.saptstartsrv_path, self.sap_instance_profile, self.sidadm))

        if self._get_status() == 0:
            logger.info(
                'sapstartsrv for SAP Instance %s_%s started: %s' %
                (self.sid, self.instance_name, start_result.output))
            return ocf.OCF_SUCCESS

        logger.error(
            'sapstartsrv for SAP Instance %s_%s start failed: %s' %
            (self.sid, self.instance_name, start_result.err))

        return ocf.OCF_NOT_RUNNING

    def start(self):
        '''
        Start sapstartsrv
        '''
        self._inititialize()
        if self._chk_systemd_support():
            return self._start_systemd_style()

        return self._start_sys5_style()

    def stop(self):
        '''
        Run sapcontrol command with StopService
        '''
        self._inititialize()
        if self._get_status() == 0:
            stop_result = run_command(
                '{} -nr {} -function StopService'.format(
                    self.sapcontrol_path, self.instance_number))
            logger.info(
                'Stopping sapstartsrv of SAP Instance %s_%s: %s' %
                (self.sid, self.instance_name, stop_result.output))
            if stop_result.returncode == 0:
                return ocf.OCF_SUCCESS

            logger.error(
                'SAP Instance %s_%s stop failed: %s' %
                (self.sid, self.instance_name, stop_result.err))
            return ocf.OCF_ERR_GENERIC

        logger.info(
            'SAP Instance %s_%s already stopped' % (self.sid, self.instance_name))
        return ocf.OCF_SUCCESS

    def status(self):
        '''
        Get sapstartsrv status
        '''
        self._inititialize()
        if self._get_status() == 0:
            return ocf.OCF_SUCCESS

        return ocf.OCF_NOT_RUNNING

    def monitor(self):
        '''
        Is the sapstartsrv server process running?
        '''
        self._inititialize()
        if ocf.is_probe():
            if self._get_status() == 0:
                return ocf.OCF_SUCCESS

            return ocf.OCF_NOT_RUNNING
        '''
        For regular monitors always return success, because recover of sapstartsrv is already handeled by SAPInstance
        This might be changed in a next-generation edition
        '''
        return ocf.OCF_SUCCESS

    def validate(self):
        '''
        Validate provided parameters
        '''
        self._inititialize()
        if not re.match('^[A-Z][A-Z0-9][A-Z0-9]$', self.sid):
            logger.error('Parsing instance profile name: %s is not a valid system ID!' % self.sid)
            return ocf.OCF_ERR_ARGS

        if not re.match('^[A-Z]*[0-9][0-9]$', self.instance_name):
            logger.error(
                'Parsing instance profile name: %s is not a valid instance name!' %
                self.instance_name)
            return ocf.OCF_ERR_ARGS

        if not re.match('^[0-9][0-9]$', self.instance_number):
            logger.error(
                'Parsing instance profile name: %s is not a valid instance number!' %
                self.instance_number)
            return ocf.OCF_ERR_ARGS

        if not re.match('^[A-Za-z][A-Za-z0-9_-]*$', self.virtual_host):
            logger.error(
                'Parsing instance profile name: %s is not a valid virtual host name!' %
                self.virtual_host)
            return ocf.OCF_ERR_ARGS

        return ocf.OCF_SUCCESS


def main():
    '''
    Main method
    '''
    sapstartsrv_agent = ocf.Agent('SAPStartSrv', SHORT_DESC, LONG_DESC)

    sapstartsrv_agent.add_parameter(
        name='InstanceName',
        shortdesc='Instance name: SID_INSTANCE_VIR-HOSTNAME',
        longdesc='The full qualified SAP instance name. e.g. HA1_ASCS00_sapha1as. '
                 'Usually this is the name of the SAP instance profile.',
        content_type='string',
        required=True,
        unique=True,
        default=''
    )

    sapstartsrv_agent.add_parameter(
        name='START_PROFILE',
        shortdesc='Start profile name',
        longdesc='The name of the SAP Instance profile. Specify this parameter, if you have '
                 'changed the name of the SAP Instance profile after the default SAP installation.',
        content_type='string',
        unique=True,
        default=''
    )

    instance_full_name = ocf.get_parameter("InstanceName")  # Example: HA1_ASCS00_sapha1as
    start_profile = ocf.get_parameter("START_PROFILE")

    sapstartsrv_instance = SapStartSrv(instance_full_name)

    sapstartsrv_agent.add_action(name='start', timeout=60, handler=sapstartsrv_instance.start)
    sapstartsrv_agent.add_action(name='stop', timeout=60, handler=sapstartsrv_instance.stop)
    sapstartsrv_agent.add_action(name='status', timeout=60, handler=sapstartsrv_instance.status)
    sapstartsrv_agent.add_action(
        name='monitor', timeout=20, interval=120, handler=sapstartsrv_instance.monitor)
    sapstartsrv_agent.add_action(
        name='validate-all', timeout=5, handler=sapstartsrv_instance.validate)

    sapstartsrv_agent.run()


if __name__ == '__main__':  # pragma: no cover
    main()
