#!/opt/alt/python35/bin/python3.5
#
# imunify360-pam        Python script to manage imunify360 pam module
#                       enabled/diabled state.
#

from __future__ import print_function
from configparser import ConfigParser
from collections import OrderedDict
from distutils.version import LooseVersion
import os
import platform
import re
import shutil
import sys
import subprocess

import yaml

import argparse

CONFIG = '/etc/pam_imunify/i360.ini'
CONFIG_DOVECOT = '/etc/dovecot/dovecot.conf'
CONFIG_DOVECOT_TMPL = '/var/cpanel/templates/dovecot2.3/main.default'
CONFIG_DOVECOT_LOCAL = '/var/cpanel/templates/dovecot2.3/main.local'
CONFIG_PAM_DOVECOT = '/etc/pam.d/dovecot_imunify'
CONFIG_PAM_DOVECOT_DOMAINOWNER = '/etc/pam.d/dovecot_imunify_domainowner'
CONFIG_PROFTPD = '/etc/proftpd.conf'
CONFIG_PAM_PROFTPD = '/etc/pam.d/proftpd_imunify'
CONFIG_PUREFTPD = '/etc/pure-ftpd.conf'
CONFIG_TEMPLATE_PUREFTPD = '/var/cpanel/conf/pureftpd/local'
CONFIG_PAM_PUREFTPD = '/etc/pam.d/pure-ftpd'

PAM_UNIX_REGEX = re.compile(r'auth\s+.+?\s+pam_unix\.so')


class Output:
    def status_changed(self, services):
        enabled = []
        already_enabled = []
        disabled = []
        already_disabled = []

        services = OrderedDict(sorted(services.items(), key=lambda x: x[0]))
        for key, value in services.items():
            enabled_prev, enabled_now = value
            if enabled_now:
                if enabled_prev:
                    already_enabled.append(key)
                else:
                    enabled.append(key)
            else:
                if not enabled_prev:
                    already_disabled.append(key)
                else:
                    disabled.append(key)

        message = None
        if len(enabled) > 0:
            message = 'imunify360-pam (%s) is now enabled.' \
                % ', '.join(enabled)

        if len(already_enabled) > 0:
            message = 'imunify360-pam (%s) is already enabled.' \
                % ', '.join(already_enabled)

        if len(disabled) > 0:
            message = 'imunify360-pam (%s) is now disabled.' \
                % ', '.join(disabled)

        if len(already_disabled) > 0:
            message = 'imunify360-pam (%s) is already disabled.' \
                % ', '.join(already_disabled)

        if message:
            print(message)

    def status(self, services):
        services = OrderedDict(sorted(services.items(), key=lambda x: x[1]))
        enabled = [key for key, value in services.items() if value]
        if len(enabled) > 0:
            print('status: enabled (%s)' % ', '.join(enabled))
        else:
            print('status: disabled')

    def warning(self, *args, **kwargs):
        print('[WARNING]', *args, **kwargs)

    def error(self, *args, **kwargs):
        print('[ERROR]', *args, **kwargs)


class YamlOutput(Output):
    def __init__(self):
        self.buffer = {}

    def __del__(self):
        print(yaml.safe_dump(self.buffer, default_flow_style=False))

    def status_changed(self, services):
        for service, value in services.items():
            enabled_prev, enabled_now = value
            if 'status_changed' not in self.buffer:
                self.buffer['status_changed'] = {}
            self.buffer['status_changed'][service] = {
                'from': 'enabled' if enabled_prev else 'disabled',
                'to': 'enabled' if enabled_now else 'disabled'
            }

    def status(self, services):
        for service, enabled in services.items():
            if 'status' not in self.buffer:
                self.buffer['status'] = {}
            self.buffer['status'][service] =  \
                'enabled' if enabled else 'disabled'

    def warning(self, *args, **kwargs):
        if 'warnings' not in self.buffer:
            self.buffer['warnings'] = []
        self.buffer['warnings'].append(' '.join(args))

    def error(self, *args, **kwargs):
        if 'errors' not in self.buffer:
            self.buffer['errors'] = []
        self.buffer['errors'].append(' '.join(args))


# This function get CP name only
def get_cp_name():
    panel = None

    # cPanel check
    if os.path.isfile('/usr/local/cpanel/cpanel'):
        panel = 'cpanel'

    # Plesk check
    elif os.path.isfile('/usr/local/psa/version'):
        panel = 'plesk'

    # DirectAdmin check
    elif os.path.isfile('/usr/local/directadmin/directadmin'):
        panel = 'directadmin'

    return panel


def readlink_f(filename):
    """
    Pythonic way of doing /bin/readlink --canonicalize filename
    and is needed for cPanel /etc/pam.d symlinks.
    """
    try:
        result = os.readlink(filename)
    except OSError:
        # not a symlink
        return filename

    if os.path.isabs(result):
        return result
    else:
        return os.path.join(os.path.dirname(filename), result)


def detect_conffiles(output=None):
    if not output:
        output = Output()

    distname = platform.linux_distribution()[0].split()[0].upper()
    if distname in ('DEBIAN', 'UBUNTU'):
        conffiles = '/etc/pam.d/common-auth',
    else:
        conffiles = '/etc/pam.d/password-auth', '/etc/pam.d/system-auth'

    if not all(os.path.exists(conf) for conf in conffiles):
        output.error("PAM configuration file(s) not found: %s" %
                     ' '.join(conffiles))
        sys.exit(1)

    return [readlink_f(fn) for fn in conffiles]


def atomic_rewrite(filename, content):
    """
    Atomically rewrites filename with given content to
    avoid possible "No space left on device"
    and unintenrional PAM module break.
    """
    shutil.copy(filename, filename + '.i360bak')
    tmp = filename + '.i360edit'
    with open(tmp, 'w') as tf:
        tf.write(content)
        st = os.stat(filename)
        os.fchmod(tf.fileno(), st.st_mode)
        os.fchown(tf.fileno(), st.st_uid, st.st_gid)
    ext = 3
    while ext > 0:
        try:
            os.rename(tmp, filename)
        except OSError:
            ext = ext - 1
            if ext == 0:
                output.error("Trouble in renaming of %s to %s" % (tmp,
                                                                  filename))
                sys.exit(1)
        else:
            ext = 0


class i360RPatch:
    def __init__(self, conf_filename, output=None):
        self._conf_filename = conf_filename
        self.output = Output() if not output else output

    def filename(self):
        return os.path.join(
            os.path.dirname(self._conf_filename),
            '.%s.i360patch' % os.path.basename(self._conf_filename))

    def create_upon(self, content):
        cmd = ['/usr/bin/diff', '--unified=1', self._conf_filename, '-']
        proc = subprocess.Popen(cmd,
                                stdin=subprocess.PIPE,
                                stdout=open(self.filename(), 'w'))
        proc.communicate(content.encode())

        if proc.returncode != 1:
            # not a big deal: will use .i360bak as the last resort
            self.output.warning("'diff -u' error", file=sys.stderr)
            os.unlink(self.filename())

    def apply(self):
        """
        :raise CalledProcessError:
        """
        cmd = ['/usr/bin/patch', '--reverse', self._conf_filename]
        subprocess.check_call(cmd,
                              stdin=open(self.filename()),
                              stdout=open('/dev/null', 'w'))
        os.unlink(self.filename())


def read_config():
    ini_pairs = open(CONFIG).read()
    config = ConfigParser()
    config.read_string("[DEFAULT]\n" + ini_pairs)
    return config['DEFAULT']


def pam_unix_patch_around(pamconfig_lines, pam_unix_ln):
    match_offset = re.search(r'success=(\d)\s+default=ignore',
                             pamconfig_lines[pam_unix_ln])

    patch_simple(pamconfig_lines, pam_unix_ln)
    pam_unix_ln += 1

    if match_offset:
        fix_offset(pamconfig_lines,
                   pam_unix_ln,
                   int(match_offset.group(1)))


def toggle_dovecot_support(enable=True, output=None):
    """
    Enable or disable pam_imunify support for Dovecot
    """
    conf = CONFIG_DOVECOT_TMPL
    if not os.path.isfile(conf):
        output.error('Dovecot config file not found. Aborting.')
        sys.exit(1)

    if not output:
        output = Output()

    passdb_regex = re.compile(r'^\s*passdb\s*\{.*?\}\s*$',
                              re.DOTALL | re.MULTILINE)

    data = open(conf).read()

    if enable:
        def passdb_replace(match):
            repl = re.sub(r'driver\s*=.*', 'driver = pam', match.group(0))
            repl = re.sub(r'args\s*=.*',
                          r'args = '
                          r'[% IF allow_domainowner_mail_pass %]'
                          r'dovecot_imunify_domainowner'
                          r'[% ELSE %]dovecot_imunify[% END %]',
                          repl)
            return repl

        data = re.sub(passdb_regex, passdb_replace, data)

        if not options.dry:
            with open(CONFIG_DOVECOT_LOCAL, 'w') as f:
                f.write(data)
        else:
            return
    else:
        os.unlink(CONFIG_DOVECOT_LOCAL)

    if os.path.isfile("/scripts/builddovecotconf"):
        subprocess.call(["/scripts/builddovecotconf"])
    if os.path.isfile("/scripts/restartsrv_dovecot"):
        subprocess.call(["/scripts/restartsrv_dovecot"])


def toggle_proftpd_support(enable=True, output=None):
    """
    Enable or disable pam_imunify support for ProFTPd
    """
    conf = CONFIG_PROFTPD
    if not os.path.isfile(conf):
        output.error('ProFTPD config file not found. Aborting.')
        sys.exit(1)

    if enable:
        version_output = subprocess.check_output(
            ['in.proftpd' if get_cp_name() == 'plesk' else 'proftpd',
             '--version-status'],
            stderr=subprocess.DEVNULL).decode(sys.stdout.encoding)
        if 'mod_auth_pam' not in version_output:
                output.error('ProFTPD built without PAM support. '
                             'pam_imunify for FTP not enabled.')
                sys.exit(1)

        version_regex = re.compile(r'ProFTPD Version:\s([0-9a-z\.]+)')
        version_found = version_regex.search(version_output)
        if version_found:
            version = LooseVersion(version_found.group(1))
            if version < LooseVersion('1.3.6c') \
               or version.vstring.startswith('1.3.6rc'):
                output.error('ProFTPD not supports needed patch. '
                             'It will be released soon. '
                             'pam_imunify for FTP not enabled.')
                sys.exit(1)

    if not output:
        output = Output()

    authpam_imunify = 'AuthOrder mod_auth_pam.c* mod_auth_file.c\n' \
                      'AuthPAM on\n' \
                      'AuthPAMConfig proftpd_imunify\n'

    authpam_regex = re.compile(r'(^AuthPAM.*\n?)+', re.MULTILINE)

    data = open(conf).read()
    authpam_found = authpam_regex.search(data)

    if enable:
        if authpam_found:
            authpam_span = authpam_found.span()
            data = data[:authpam_span[0]] + authpam_imunify \
                + data[authpam_span[1]:]
        else:
            data = authpam_imunify + '\n' + data

        if not options.dry:
            atomic_rewrite(conf, data)
        else:
            return
    else:
        conf_bak = conf + '.i360bak'
        if os.path.isfile(conf_bak):
            os.rename(conf_bak, conf)
        else:
            output.error("Failed to disable proftpd integration: %s not found"
                         % conf_bak)
            sys.exit(1)
    if os.path.isfile("/scripts/restartsrv_ftpd"):
        subprocess.call(["/scripts/restartsrv_ftpd"])


def toggle_pureftpd_support(enable=True, output=None):
    """
    Enable or disable pam_imunify support for PureFTPd
    """
    conf = CONFIG_PUREFTPD
    if not os.path.isfile(conf):
        output.error('Pure-FTPd config file not found. Aborting.')
        sys.exit(1)
    if enable:
        # Pure-FTPd writes output to stdin, so we have to use
        # pipes to read from stdin afterwards...
        pipe_r, pipe_w = os.pipe()

        try:
            subprocess.check_output(
                ['pure-ftpd', '-l', 'pam'],
                stdin=pipe_w,
                stderr=subprocess.STDOUT,
                timeout=1)
        except subprocess.CalledProcessError:
            os.close(pipe_w)
            if os.fdopen(pipe_r).read().startswith('421 '):
                output.error('Pure-FTPd built without PAM support. '
                             'pam_imunify for FTP not enabled.')
                sys.exit(1)
        except subprocess.TimeoutExpired:
            # This could happen if pam is supported
            # and pure-ftpd has started
            pass

        os.close(pipe_w)
        os.close(pipe_r)

    if not output:
        output = Output()

    authext_imunify = 'ExtAuth /var/run/ftpd.imunify360.sock'

    extauth_regex = re.compile(r'^\s*ExtAuth\s.*$', re.MULTILINE)

    data = open(conf).read()
    extauth_found = extauth_regex.search(data)

    full_string_found = False
    with open(conf) as file_in:
        for line in file_in:
            if line == authext_imunify:
                full_string_found = True

    if enable:
        if not full_string_found:
            if extauth_found:
                extauth_span = extauth_found.span()
                data = data[:extauth_span[0]] + authext_imunify \
                    + data[extauth_span[1]:]
            else:
                data = authpam_imunify + '\n' + data

        if not options.dry:
            atomic_rewrite(conf, data)
    else:
        conf_bak = conf + '.i360bak'
        if os.path.isfile(conf_bak):
            shutil.move(conf_bak, conf)
        else:
            output.error("Failed to disable pure-ftpd integration: "
                         "%s not found" % conf_bak)
            sys.exit(1)
    if enable:
        if options.dry:
            return
        conf_bak_tpl = CONFIG_TEMPLATE_PUREFTPD + '.i360bak'
        if (not os.path.isfile(conf_bak_tpl) and
                os.path.isfile(CONFIG_TEMPLATE_PUREFTPD)):
            os.rename(CONFIG_TEMPLATE_PUREFTPD, conf_bak_tpl)

        needle = "ExtAuth: /var/run/ftpd.imunify360.sock"
        if os.path.isfile(CONFIG_TEMPLATE_PUREFTPD):
            with open(CONFIG_TEMPLATE_PUREFTPD, "r+") as file:
                for line in file:
                    if needle in line:
                        break
                else:  # not found, we are at the eof
                    file.write(needle)
        else:
            with open(CONFIG_TEMPLATE_PUREFTPD, 'w') as out_file:
                out_file.write(needle)
    else:
        conf_bak_tpl = CONFIG_TEMPLATE_PUREFTPD + '.i360bak'
        if os.path.isfile(conf_bak_tpl):
            os.rename(conf_bak_tpl, CONFIG_TEMPLATE_PUREFTPD)
    if os.path.isfile("/scripts/restartsrv_ftpd"):
        subprocess.call(["/scripts/restartsrv_ftpd"])
# /usr/local/cpanel/scripts/setupftpserver pure-ftpd --force


def toggle_sshd_support(conffiles, enable=True, output=None):
    """
    Enable or disable pam_imunify module for sshd authentication
    """
    if not output:
        output = Output()

    if enable:
        for conf in conffiles:
            lines = open(conf).readlines()

            try:
                pam_unix_ln = next(
                    ln for ln, line in enumerate(lines)
                    if PAM_UNIX_REGEX.search(line)
                    )
            except StopIteration:
                output.error("PAM configuration file %s parse error" % conf)
                sys.exit(1)

            pam_unix_patch_around(lines, pam_unix_ln)

            content = ''.join(lines)
            i360RPatch(conf, output).create_upon(content)
            if not options.dry:
                atomic_rewrite(conf, content)
    else:
        for conf in conffiles:
            rpatch = i360RPatch(conf, output)
            if os.path.exists(rpatch.filename()):
                try:
                    rpatch.apply()
                    continue
                except subprocess.CalledProcessError as e:
                    output.warning("'patch -R' was not successful: %s" % e,
                                   file=sys.stderr)
            else:
                output.warning("File not found: %s" % rpatch.filename(),
                               file=sys.stderr)

            atomic_rewrite(conf, open(conf + '.i360bak').read())


def set_panel_integration(panel, confs, output=None):
    imunify_regex = re.compile(r'auth\s+sufficient\s+pam_imunify\.so')

    if not panel:
        return

    if not output:
        output = Output()

    for conf in confs:
        lines = open(conf).readlines()

        try:
            imunify_ln = next(
                ln for ln, line in enumerate(lines)
                if imunify_regex.search(line)
                )
        except StopIteration:
            output.error("PAM configuration file %s parse error" % conf)
            sys.exit(1)

        match_count = 0

        def panel_replace(match):
            nonlocal match_count
            if match.group() in ['cpanel', 'plesk', 'directadmin']:
                match_count += 1
                return panel
            return match.group()

        lines[imunify_ln] = re.sub(
            r'\b([^\s]+)\b', panel_replace, lines[imunify_ln])

        if match_count == 0:
            lines[imunify_ln] = '%s %s\n' % (lines[imunify_ln].strip(), panel)

        content = ''.join(lines)
        atomic_rewrite(conf, content)
        os.unlink(conf + '.i360bak')


def patch_simple(pamconfig_lines, pam_unix_ln):
    pamconfig_lines.insert(pam_unix_ln + 1,
                           'auth\trequired\tpam_imunify.so\n')
    pamconfig_lines.insert(pam_unix_ln,
                           'auth\trequired\tpam_imunify.so\tcheck_only\n')


def fix_offset(pamconfig_lines, pam_unix_ln, pam_unix_success_offset):
    bump_to = pam_unix_success_offset + 1
    pamconfig_lines[pam_unix_ln] = re.sub(r'success=\d',
                                          'success=%d' % bump_to,
                                          pamconfig_lines[pam_unix_ln])


class Cmd:
    @classmethod
    def enable(cls, conffiles, output=None):
        if not output:
            output = Output()

        if any('pam_imunify.so' in open(conf).read() for conf in conffiles):
            cls._cphulk_check()
            output.status_changed({'sshd': (True, True)})
            return

        toggle_sshd_support(conffiles, True, output)

        if not options.dry:
            cls._cphulk_check(output)
            output.status_changed({'sshd': (False, True)})

    @staticmethod
    def enable_dovecot(conffiles, output=None):
        if not output:
            output = Output()

        panel = get_cp_name()
        if panel:
            set_panel_integration(
                panel,
                [CONFIG_PAM_DOVECOT, CONFIG_PAM_DOVECOT_DOMAINOWNER],
                output)
        else:
            output.error('No supported panel found.')
            if options.dry:
                sys.exit(1)
            return

        dovecot_enabled = os.path.isfile(CONFIG_DOVECOT) and \
            'dovecot_imunify' in open(CONFIG_DOVECOT).read()

        if not dovecot_enabled:
            toggle_dovecot_support(True, output)

        if not options.dry:
            output.status_changed({'dovecot': (dovecot_enabled, True)})

    @staticmethod
    def enable_proftpd(conffiles, output=None):
        if not output:
            output = Output()

        panel = get_cp_name()
        if panel:
            set_panel_integration(panel, [CONFIG_PAM_PROFTPD], output)
        else:
            output.error('No supported panel found.')
            return

        proftpd_enabled = os.path.isfile(CONFIG_PROFTPD) and \
            'proftpd_imunify' in open(CONFIG_PROFTPD).read()

        if not proftpd_enabled:
            toggle_proftpd_support(True, output)

        if not options.dry:
            output.status_changed({'ftp': (proftpd_enabled, True)})

    @staticmethod
    def enable_pureftpd(conffiles, output=None):
        if not output:
            output = Output()

        panel = get_cp_name()

        pureftpd_enabled = os.path.isfile(CONFIG_PUREFTPD) and \
            '/var/run/ftpd.imunify360.sock' in open(CONFIG_PUREFTPD).read()

        if not pureftpd_enabled:
            toggle_pureftpd_support(True, output)

        if not options.dry:
            output.status_changed({'ftp': (pureftpd_enabled, True)})

    @staticmethod
    def disable_all(conffiles, output=None):
        if not output:
            output = Output()

        dovecot_enabled = os.path.isfile(CONFIG_DOVECOT) and \
            'dovecot_imunify' in open(CONFIG_DOVECOT).read()
        proftpd_enabled = os.path.isfile(CONFIG_PROFTPD) and \
            'proftpd_imunify' in open(CONFIG_PROFTPD).read()
        pureftpd_enabled = os.path.isfile(CONFIG_PUREFTPD) and \
            '/var/run/ftpd.imunify360.sock' in open(CONFIG_PUREFTPD).read()
        sshd_enabled = any('pam_imunify.so' in open(conf).read()
                           for conf in conffiles)

        if dovecot_enabled:
            toggle_dovecot_support(False, output)

        if proftpd_enabled:
            toggle_proftpd_support(False, output)

        if pureftpd_enabled:
            toggle_pureftpd_support(False, output)

        if sshd_enabled:
            toggle_sshd_support(conffiles, False, output)

        output.status_changed({
            'sshd': (sshd_enabled, False),
            'dovecot': (dovecot_enabled, False),
            'ftp': (proftpd_enabled or pureftpd_enabled, False)
        })

    @staticmethod
    def disable(conffiles, output=None):
        if not output:
            output = Output()

        sshd_enabled = any('pam_imunify.so' in open(conf).read()
                           for conf in conffiles)

        if sshd_enabled:
            toggle_sshd_support(conffiles, False, output)

        output.status_changed({'sshd': (sshd_enabled, False)})

    @staticmethod
    def disable_dovecot(conffiles, output=None):
        if not output:
            output = Output()

        dovecot_enabled = os.path.isfile(CONFIG_DOVECOT) and \
            'dovecot_imunify' in open(CONFIG_DOVECOT).read()
        if dovecot_enabled:
            toggle_dovecot_support(False, output)

        output.status_changed({'dovecot': (dovecot_enabled, False)})

    @staticmethod
    def disable_proftpd(conffiles, output=None):
        if not output:
            output = Output()

        proftpd_enabled = os.path.isfile(CONFIG_PROFTPD) and \
            'proftpd_imunify' in open(CONFIG_PROFTPD).read()
        if proftpd_enabled:
            toggle_proftpd_support(False, output)

        output.status_changed({'ftp': (proftpd_enabled, False)})

    @staticmethod
    def disable_pureftpd(conffiles, output=None):
        if not output:
            output = Output()

        pureftpd_enabled = os.path.isfile(CONFIG_PUREFTPD) and \
            '/var/run/ftpd.imunify360.sock' in open(CONFIG_PUREFTPD).read()
        if pureftpd_enabled:
            toggle_pureftpd_support(False, output)

        output.status_changed({'ftp': (pureftpd_enabled, False)})

    @staticmethod
    def enable_ftp(conffiles, output=None):
        if not output:
            output = Output()

        panel = get_cp_name()
        if panel:
            if panel == 'cpanel':
                if os.path.isfile("/var/cpanel/cpanel.config"):
                    ftp = ""
                    with open("/var/cpanel/cpanel.config", "r") as cpcfg:
                        data = cpcfg.read()
                        if 'ftpserver=proftpd' in data:
                            Cmd.enable_proftpd(conffiles, output)
                        elif 'ftpserver=pure-ftpd' in data:
                            Cmd.enable_pureftpd(conffiles, output)
                        else:
                            output.error('No supported FTP found.')
                            if options.dry:
                                sys.exit(1)
                    return
        output.error('No supported panel found.')
        if options.dry:
            sys.exit(1)
        return

    @staticmethod
    def disable_ftp(conffiles, output=None):
        if not output:
            output = Output()

        proftpd_enabled = os.path.isfile(CONFIG_PROFTPD) and \
            'proftpd_imunify' in open(CONFIG_PROFTPD).read()
        pureftpd_enabled = os.path.isfile(CONFIG_PUREFTPD) and \
            '/var/run/ftpd.imunify360.sock' in open(CONFIG_PUREFTPD).read()

        if proftpd_enabled:
            toggle_proftpd_support(False, output)

        if pureftpd_enabled:
            toggle_pureftpd_support(False, output)

        output.status_changed({
            'ftp': (proftpd_enabled or pureftpd_enabled, False)
        })

    @classmethod
    def status(cls, conffiles, output=None):
        if not output:
            output = Output()

        dovecot_enabled = os.path.isfile(CONFIG_DOVECOT) and \
            'dovecot_imunify' in open(CONFIG_DOVECOT).read()
        sshd_enabled = any('pam_imunify.so' in open(conf).read()
                           for conf in conffiles)
        proftpd_enabled = os.path.isfile(CONFIG_PROFTPD) and \
            'proftpd_imunify' in open(CONFIG_PROFTPD).read()
        pureftpd_enabled = os.path.isfile(CONFIG_PUREFTPD) and \
            '/var/run/ftpd.imunify360.sock' in open(CONFIG_PUREFTPD).read()

        if dovecot_enabled or sshd_enabled \
                or proftpd_enabled or pureftpd_enabled:
            cls._cphulk_check(output)

        output.status({'sshd': sshd_enabled,
                       'dovecot': dovecot_enabled,
                       'ftp': proftpd_enabled or pureftpd_enabled})

    @staticmethod
    def _cphulk_check(output=None):
        if not os.path.isfile("/usr/sbin/whmapi1"):
            return
        if not read_config().getboolean('verbose'):
            return

        if not output:
            output = Output()

        proc = subprocess.run(['/usr/sbin/whmapi1',
                               'servicestatus',
                               'service=cphulkd'],
                              stdin=subprocess.DEVNULL,
                              stdout=subprocess.PIPE)
        if proc.returncode != 0:
            # we expect err dump is printed to stderr
            return

        try:
            status = yaml.load(proc.stdout)
            if status['data']['service'][0]['enabled']:
                output.warning("cPHulk is enabled", file=sys.stderr)
        except (yaml.YAMLError, IndexError, KeyError) as e:
            output.warning("whmapi error:", e, file=sys.stderr)


if __name__ == '__main__':
    output = Output()
    parser = argparse.ArgumentParser()
    parser.add_argument("-r",
                        "--dry-run",
                        dest='dry',
                        action='store_true',
                        help="Dry run the command, whithout changing of state")
    options, unknown = parser.parse_known_args()
    try:
        cmd = getattr(Cmd, sys.argv[1].replace('-', '_'))
        if len(sys.argv) > 2 and sys.argv[2] == '--yaml':
            output = YamlOutput()
    except (AttributeError, IndexError):
        sys.exit("Usage: %s {enable|enable-dovecot|"
                 "enable-proftpd|enable-pureftpd|"
                 "disable|disable-dovecot|"
                 "disable-proftpd|disable-pureftpd|"
                 "disable-all|enable-ftp|disable-ftp|status}" % sys.argv[0])
    else:
        try:
            cmd(detect_conffiles(output), output)
        except SystemExit:
            del output
            raise
