#!/usr/libexec/platform-python
#
#  Manage a set of links into a DRBD shared directory
#
#  Written by: Sean Reifschneider <jafo@tummy.com>
#  Copyright (c) 2004-2013, tummy.com, ltd.  All Rights Reserved
#  drbdlinks is under the following license: GPLv2

import os
import sys
import stat
import syslog
import shutil
import glob
import subprocess

configFile = '/etc/drbdlinks.conf'
configDir = '/etc/drbdlinks.d/*.conf'
cleanConfigsDirectory = '/var/lib/drbdlinks/configs-to-clean'


syslog.openlog('drbdlinks', syslog.LOG_PID)

try:
    import optparse
except ImportError:
    import optik
    optparse = optik

try:
    from subprocess import DEVNULL
except ImportError:
    DEVNULL = open(os.devnull, 'wb')

try:
    execfile
except NameError:
    def execfile(filepath, globals=None, locals=None):
        with open(filepath, 'rb') as file:
            exec(compile(file.read(), filepath, 'exec'), globals, locals)


class lsb:
    class statusRC:
        OK = 0
        VAR_PID = 1
        VAR_LOCK = 2
        STOPPED = 3
        UNKNOWN = 4
        LSBRESERVED = 5
        DISTRESERVED = 100
        APPRESERVED = 150
        RESERVED = 200

    class exitRC:
        OK = 0
        GENERIC = 1
        EINVAL = 2
        ENOTSUPPORTED = 3
        EPERM = 4
        NOTINSTALLED = 5
        NOTCONFIGED = 6
        NOTRUNNING = 7
        LSBRESERVED = 8
        DISTRESERVED = 100
        APPRESERVED = 150
        RESERVED = 200


###########
def log(s):
    sys.stderr.write(s)
    syslog.syslog(s)


##############################################
def multiInitRestart(flavor, initscript_list):
    for initscript in initscript_list:
        if os.path.exists(initscript):
            if initscript.endswith(".service"):
                retcode = os.system(
                    'systemctl restart %s' % initscript.rsplit('/', 1)[1])
            else:
                retcode = os.system('%s restart' % initscript)
            if retcode != 0:
                log('%s restart returned %d, expected 0' % (flavor, retcode))
            return(retcode != 0)

    syslog.syslog(
        'Unable to locate %s init script, not restarting.' % flavor)
    return(0)


##########################
def restartSyslog(config):
    if not config.restartSyslog:
        return(0)

    return multiInitRestart(
        'syslog', ['/etc/init.d/syslog', '/etc/init.d/rsyslog',
                   '/usr/lib/systemd/system/rsyslog.service'])


########################
def restartCron(config):
    if not config.restartCron:
        return(0)

    return multiInitRestart(
        'cron', ['/etc/init.d/crond', '/etc/init.d/cron',
                 '/usr/lib/systemd/system/crond.service'])


#######################
def testConfig(config):
    allUp = True
    for linkLocal, linkDest, useBindLink in config.linkList:
        suffixName = linkLocal + options.suffix
        #  check to see if the link is in place
        if not os.path.exists(suffixName):
            allUp = False
            if options.verbose >= 1:
                print(
                    'testConfig: Original file not present: "%s"' % suffixName)
            continue

    if options.verbose >= 1:
        print('testConfig: Returning %s' % allUp)
    return(allUp)


###############################
def loadConfigFile(configFile):
    class configClass:
        def __init__(self):
            self.mountpoint = None
            self.cleanthisconfig = 0
            self.linkList = []
            self.useSELinux = 0
            self.selinuxenabledPath = None
            self.useBindMount = 0
            self.debug = 0
            self.restartSyslog = 0
            self.restartCron = 0
            self.makeMountpointShared = 0

            #  Locate where the selinuxenabled binary is
            for path in (
                    '/usr/sbin/selinuxenabled',
                    '/sbin/selinuxenabled', ):
                if os.path.exists(path):
                    self.selinuxenabledPath = path
                    break

            #  auto-detect if SELinux is on
            if self.selinuxenabledPath:
                ret = os.system(self.selinuxenabledPath)
                if ret == 0:
                    self.useSELinux = 1

            #  detect what ls(1) supports to show SELinux context
            try:
                subprocess.check_call(
                    ['ls', '--scontext', __file__],
                    stdout=DEVNULL, stderr=DEVNULL)
                self.showContextCommand = 'ls -d --scontext "%s"'
            except subprocess.CalledProcessError:
                self.showContextCommand = 'ls -d -Z -1 "%s"'

        def cmd_cleanthisconfig(self, enabled=1):
            self.cleanthisconfig = enabled

        def cmd_mountpoint(self, arg, shared=0):
            self.mountpoint = arg
            if shared:
                self.makeMountpointShared = 1

        def cmd_link(self, src, dest=None):
            self.linkList.append((src, dest, self.useBindMount))

        def cmd_selinux(self, enabled=1):
            self.useSELinux = enabled

        def cmd_usebindmount(self, enabled=1):
            self.useBindMount = enabled

        def cmd_debug(self, level=1):
            self.debug = level

        def cmd_restartSyslog(self, enabled=1):
            self.restartSyslog = enabled

        def cmd_restartCron(self, enabled=1):
            self.restartCron = enabled

    #  set up config environment
    config = configClass()
    namespace = {
        'mountpoint': config.cmd_mountpoint,
        'link': config.cmd_link,
        'selinux': config.cmd_selinux,
        'debug': config.cmd_debug,
        'usebindmount': config.cmd_usebindmount,
        'restartSyslog': config.cmd_restartSyslog,
        'restartsyslog': config.cmd_restartSyslog,
        'restartCron': config.cmd_restartCron,
        'restartcron': config.cmd_restartCron,
        'cleanthisconfig': config.cmd_cleanthisconfig,
    }

    #  load the files
    for filename in [configFile] + sorted(glob.iglob(configDir)):
        try:
            execfile(filename, {}, namespace)
        except Exception:
            print(
                'ERROR: Loading configuration file "%s" failed.  '
                'See below for details:' % filename)
            print('Environment: %s' % repr(
                ['%s=%s' % i for i in os.environ.items()
                    if i[0].startswith('OCF_')]))
            raise

    #  process the data we got
    if config.mountpoint:
        config.mountpoint = config.mountpoint.rstrip('/')
    for i in range(len(config.linkList)):
        oldList = config.linkList[i]
        if oldList[1]:
            arg2 = oldList[1].rstrip('/')
        else:
            if not config.mountpoint:
                log(
                    'ERROR: Used link() when no mountpoint() was set '
                    'in the config file.\n')
                sys.exit(3)
            arg2 = oldList[0].lstrip('/')
            arg2 = os.path.join(config.mountpoint, arg2).rstrip('/')
        config.linkList[i] = (
            [oldList[0].rstrip('/'), arg2] + list(oldList[2:]))

    #  return the data
    return(config)


def print_metadata():
    print('''
<?xml version="1.0"?>
<!DOCTYPE resource-agent SYSTEM "ra-api-1.dtd">

<!-- Root element: give the name of the Resource agent -->
<resource-agent name="drbdlinks" version="@@@VERSION@@@">

<!-- Version number of the standard this complies with -->
<version>1.0</version>

<!-- List all the instance parameters the RA supports or requires. -->
<parameters>

<!-- Note that parameters flagged with 'unique' must be unique; ie no
    other resource instance of this resource type may have the same set
    of unique parameters.
-->

<parameter name="configfile" unique="1">
<longdesc lang="en">
The full path of the configuration file on disc.  The default is
/etc/drbdlinks.conf, but you may wish to store this on the shared
storage, or have different config files if you have multiple
resource groups.
</longdesc>

<shortdesc lang="en">Configuration Filename</shortdesc>

<content type="string" default="/etc/drbdlinks.conf" />

</parameter>

<parameter name="suffix">
<longdesc lang="en">
The suffix of the files/directories that are moved out of the way on the host
file-system to make room for the symlink.  By default this is ".drbdlinks".
</longdesc>

<shortdesc lang="en">Host Filename Suffix</shortdesc>

<content type="string" default=".drbdlinks" />

</parameter>

</parameters>

<!-- List the actions supported by the RA -->
<actions>

<action name="start"   timeout="1m" />
<action name="stop"    timeout="1m" />
<action name="monitor" depth="0"  timeout="20" interval="10" />
<action name="meta-data"  timeout="5" />

</actions>

</resource-agent>
'''.strip())
    sys.exit(lsb.exitRC.OK)


#  meta-data may happen when configuration is not present.
if 'meta-data' in sys.argv:
    print_metadata()

#  parse arguments
parser = optparse.OptionParser()
parser.add_option(
    '-c', '--config-file', dest='configFile', type='string',
    default=configFile,
    help='Location of the configuration file.')
parser.add_option(
    '-s', '--suffix', dest='suffix', type='string',
    default='.drbdlinks',
    help='Name to append to the local file-system name when the link '
            'is in place.')
parser.add_option(
    '-v', '--verbose', default=0,
    dest='verbose', action='count',
    help='Increase verbosity level by 1 for every "-v".')
parser.set_usage(
    '%prog (start|stop|auto|status|monitor|list|checklinks|\n'
    '        initialize_shared_storage)')
options, args = parser.parse_args()
origConfigFile = configFile

#  if called from OCF, parse the environment
OCFenabled = os.environ.get('OCF_RA_VERSION_MAJOR') is not None
if OCFenabled:
    try:
        options.configFile = os.environ['OCF_RESKEY_configfile']
    except KeyError:
        pass
    try:
        options.suffix = os.environ['OCF_RESKEY_suffix']
    except KeyError:
        pass
configFile = options.configFile

#  figure out what the mode to run in
if len(args) == 1 or (OCFenabled and len(args) > 0):
    #  NOTE: OCF specifies that additional arguments beyond the first should
    #  be ignored.
    if args[0] not in (
            'start', 'stop', 'auto', 'monitor', 'status', 'list',
            'checklinks', 'initialize_shared_storage'):
        parser.error(
            'ERROR: Unknown mode "%s", expecting one of '
            '(start|stop|auto|\n      status|monitor|list|checklinks|'
            'initialize_shared_storage)' % args[0])
        sys.exit(lsb.exitRC.ENOTSUPPORTED)
    mode = args[0]
else:
    parser.error('Expected exactly one argument to specify the mode.')
    sys.exit(lsb.exitRC.EINVAL)
if options.verbose >= 2:
    print('Initial mode: "%s"' % mode)

#  load config file
try:
    config = loadConfigFile(configFile)
except IOError as e:
    if e.errno == 2:
        if mode == 'monitor' or mode == 'status':
            print(
                'WARNING: Config file "%s" not found, assuming drbdlinks '
                'is stopped' % configFile)
            if mode == 'status':
                sys.exit(lsb.statusRC.STOPPED)
            else:
                sys.exit(lsb.exitRC.NOTRUNNING)
        print('ERROR: Unable to open config file "%s":' % configFile)
        print('  %s' % e)
        syslog.syslog('Invalid config file "%s"' % configFile)
        sys.exit(lsb.statusRC.UNKNOWN)
    raise
if not config.mountpoint:
    log('No mountpoint found in config file.  Aborting.\n')
    if mode == 'monitor':
        if config.debug:
            syslog.syslog('Monitor called without mount point')
        sys.exit(lsb.exitRC.EINVAL)
    if config.debug:
        syslog.syslog('No mount point')
    sys.exit(lsb.statusRC.UNKNOWN)
if not os.path.exists(config.mountpoint):
    log('Mountpoint "%s" does not exist.  Aborting.\n' % config.mountpoint)
    if mode == 'monitor':
        if config.debug:
            syslog.syslog('Mount point does not exist, monitor mode')
        sys.exit(lsb.exitRC.EINVAL)
    if config.debug:
        syslog.syslog('Mount point does not exist')
    sys.exit(lsb.statusRC.UNKNOWN)

#  startup log message
if config.debug:
    syslog.syslog(
        'drbdlinks starting: args: "%s", configfile: "%s"'
        % (repr(sys.argv), configFile))

#  if mode is auto, figure out what mode to use
if mode == 'auto':
    if (os.stat(config.mountpoint).st_dev !=
            os.stat(os.path.join(config.mountpoint, '..')).st_dev):
        if options.verbose >= 1:
            print('Detected mounted file-system on "%s"' % config.mountpoint)
        mode = 'start'
    else:
        mode = 'stop'
if options.verbose >= 1:
    print('Mode: "%s"' % mode)

# just display the list of links
if mode == 'list':
    for linkLocal, linkDest, useBindMount in config.linkList:
        print('%s %s %s' % (linkLocal, linkDest, useBindMount))
    sys.exit(0)

#  set up links
anyLinksChanged = 0
if mode == 'start':
    errorCount = 0

    #  set up shared mountpoint
    if config.makeMountpointShared:
        os.system('mount --make-shared  "%s"' % config.mountpoint)

    #  loop over links
    for linkLocal, linkDest, useBindMount in config.linkList:
        suffixName = linkLocal + options.suffix
        #  check to see if the link is in place
        if os.path.exists(suffixName):
            if options.verbose >= 1:
                print(
                    'Skipping, appears to already be linked: "%s"' % linkLocal)
            continue

        #  make the link
        try:
            if options.verbose >= 2:
                print('Renaming "%s" to "%s"' % (linkLocal, suffixName))
            os.rename(linkLocal, suffixName)
            anyLinksChanged = 1
        except (OSError, IOError) as e:
            log(
                'Error renaming "%s" to "%s": %s\n'
                % (suffixName, linkLocal, str(e)))
            errorCount = errorCount + 1
            if options.verbose >= 2:
                print('Linking "%s" to "%s"' % (linkDest, linkLocal))
            anyLinksChanged = 1

        if useBindMount:
            st = os.stat(linkDest)
            if stat.S_ISREG(st.st_mode):
                open(linkLocal, 'w').close()
            else:
                os.mkdir(linkLocal)
            os.system('mount -o bind "%s" "%s"' % (linkDest, linkLocal))
        else:
            try:
                os.symlink(linkDest, linkLocal)
            except (OSError, IOError) as e:
                log(
                    'Error linking "%s" to "%s": %s'
                    % (linkDest, linkLocal, str(e)))
                errorCount = errorCount + 1

        #  set up in SELinux
        if config.useSELinux:
            fp = os.popen(config.showContextCommand % suffixName, 'r')
            line = fp.readline()
            fp.close()
            if line:
                line = line.split(' ')[0]
                seInfo = line.split(':')
                seUser, seRole, seType = seInfo[:3]
                if len(seInfo) >= 4:
                    seRange = seInfo[3]
                    os.system(
                        'chcon -h -u "%s" -r "%s" -t "%s" -l "%s" "%s"'
                        % (seUser, seRole, seType, seRange, linkLocal))
                else:
                    os.system(
                        'chcon -h -u "%s" -r "%s" -t "%s" "%s"'
                        % (seUser, seRole, seType, linkLocal))

    if anyLinksChanged:
        if restartSyslog(config):
            errorCount = errorCount + 1
        if restartCron(config):
            errorCount = errorCount + 1

    if config.cleanthisconfig and origConfigFile != configFile:
        if not os.path.exists(cleanConfigsDirectory):
            if config.debug:
                syslog.syslog(
                    'Config copy directory "%s" does not exist.'
                    % cleanConfigsDirectory)
        else:
            if config.debug:
                syslog.syslog('Preserving a copy of the config file.')
            shutil.copy(configFile, cleanConfigsDirectory)

    if errorCount:
        if config.debug:
            syslog.syslog('Exiting due to %d errors' % errorCount)
        sys.exit(lsb.exitRC.GENERIC)
    if config.debug:
        syslog.syslog('Exiting with no errors')
    sys.exit(lsb.exitRC.OK)

#  remove links
elif mode == 'stop':
    errorCount = 0
    for linkLocal, linkDest, useBindMount in config.linkList:
        suffixName = linkLocal + options.suffix
        #  check to see if the link is in place
        if not os.path.exists(suffixName):
            if options.verbose >= 1:
                print(
                    'Skipping, appears to already be shut down: "%s"'
                    % linkLocal)
            continue

        #  break the link
        try:
            if options.verbose >= 2:
                print('Removing "%s"' % (linkLocal,))
            anyLinksChanged = 1

            if useBindMount:
                os.system('umount "%s"' % linkLocal)
                try:
                    os.remove(linkLocal)
                except Exception:
                    pass
                if os.path.exists(linkLocal):
                    os.rmdir(linkLocal)
            else:
                os.remove(linkLocal)
        except (OSError, IOError) as e:
            log('Error removing "%s": %s\n' % (linkLocal, str(e)))
            errorCount = errorCount + 1
        try:
            if options.verbose >= 2:
                print('Renaming "%s" to "%s"' % (suffixName, linkLocal))
            os.rename(suffixName, linkLocal)
            anyLinksChanged = 1
        except (OSError, IOError) as e:
            log(
                'Error renaming "%s" to "%s": %s\n'
                % (suffixName, linkLocal, str(e)))
            errorCount = errorCount + 1

    if anyLinksChanged:
        restartSyslog(config)
        restartCron(config)

    if errorCount:
        if config.debug:
            syslog.syslog('Exiting due to %d errors' % errorCount)
        sys.exit(lsb.exitRC.GENERIC)
    if config.debug:
        syslog.syslog('Exiting with no errors')
    sys.exit(lsb.exitRC.OK)

#  monitor mode
elif mode == 'monitor':
    if testConfig(config):
        if config.debug:
            syslog.syslog('Monitor mode returning ok')
        sys.exit(lsb.exitRC.OK)

    if config.debug:
        syslog.syslog('Monitor mode returning notrunning')
    sys.exit(lsb.exitRC.NOTRUNNING)

#  status mode
elif mode == 'status':
    if testConfig(config):
        print("info: DRBD Links OK (present)")
        if config.debug:
            syslog.syslog('Status mode returning ok')
        sys.exit(lsb.statusRC.OK)

    print("info: DRBD Links stopped (not set up)")
    if config.debug:
        syslog.syslog('Status mode returning stopped')
    sys.exit(lsb.statusRC.STOPPED)

#  check mode
elif mode == 'checklinks':
    for linkLocal, linkDest, useBindMount in config.linkList:
        if not os.path.exists(linkDest):
            print('Does not exist: %s' % linkDest)
    sys.exit(lsb.exitRC.OK)


#  initialize_shared_storage mode
elif mode == 'initialize_shared_storage':
    def dirs_to_make(src, dest):
        '''Return a list of paths, from top to bottom, that need to be created
        in the destination.  The return value is a list of tuples of
        (src, dest) where the `src` is the corresponding source directory to
        the `dest`.
        '''
        retval = []
        destdirname = os.path.dirname(dest)
        srcdirname = os.path.dirname(src)
        while srcdirname and srcdirname != '/':
            if os.path.exists(destdirname):
                break
            if os.path.basename(destdirname) != os.path.basename(srcdirname):
                break
            retval.append((srcdirname, destdirname))
            srcdirname = os.path.dirname(srcdirname)
            destdirname = os.path.dirname(destdirname)
        return retval[::-1]

    relative_symlinks = []
    for linkLocal, linkDest, useBindMount in config.linkList:
        if os.path.exists(linkDest):
            continue

        for src, dest in dirs_to_make(linkLocal, linkDest):
            print('Making directory "%s"' % dest)
            os.mkdir(dest)
            srcstat = os.stat(src)
            os.chmod(dest, srcstat.st_mode)
            os.chown(dest, srcstat.st_uid, srcstat.st_gid)

        print('Copying "%s" to "%s"' % (linkLocal, linkDest))
        os.system('cp -ar "%s" "%s"' % (linkLocal, linkDest))

        fp = os.popen('find "%s" -type l -lname "[^/].*" -print0' % linkLocal)
        relative_symlinks += [l for l in fp.read().split('\0') if l]
        fp.close()

    if relative_symlinks:
        print(
            '\nWARNING: The following copied files contain relative '
            'symlinks:\n')
        for symlink in relative_symlinks:
            print('   %s -> %s' % (symlink, os.readlink(symlink)))

    sys.exit(lsb.exitRC.OK)
