# Copyright 1999-2014. Parallels IP Holdings GmbH. All Rights Reserved.
""" f2bmng - Fail2Ban configuration and service management utility. """
import sys
import os
import errno
import json
import ConfigParser
from glob import glob
import subprocess
import socket
from distutils.version import LooseVersion

try:
    import cpickle as pickle
except ImportError:
    import pickle

from optmatch import OptionMatcher, optmatcher, optset, UsageException
import plesk_log
import plesk_service
import plesk_config
from os_detect import OS

from f2b_cfg_array import ConfigurationArray


log = plesk_log.getLogger(__name__)


def get_packages_by_files(files):
    """ Given a list of files (absolute paths) returns a list of package names (which may be None), each
        entry of which corresponds to input file list.
    """
    if OS.is_deb_based():
        command = ['/usr/bin/dpkg', '-S'] + list(files)

        def output_line_to_package(line):
            if line.startswith('dpkg: ') and line.endswith(' not found.'):      # dpkg 1.15 (deb6, ubt1004)
                return None
            if line.startswith('dpkg-query: no path found matching pattern '):  # dpkg 1.16 (deb7, ubt1204)
                return None
            return line.split(': ')[0]
    else:
        command = ['/bin/rpm', '-qf', '--queryformat', r'%{NAME}\n'] + list(files)

        def output_line_to_package(line):
            if line.startswith('error: ') or line.endswith('is not owned by any package'):
                return None
            return line

    process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, 
                               bufsize=-1, close_fds=True, universal_newlines=True)

    output = process.communicate()[0]
    lines = filter(None, output.split(os.linesep))

    retcode = process.poll()
    # In case of unpackaged files dpkg returns 1, while rpm returns number of such files o_O
    if retcode and len(lines) != len(files):
        raise subprocess.CalledProcessError(retcode, command)
    if len(lines) != len(files):
        raise Exception("Failed to get packages by files: package manager returned %d lines instead of expected %d" %
                        (len(lines), len(files)))

    return map(output_line_to_package, lines)

def get_product_root_d():
    product_root_d = "/usr/local/psa"
    try:
        product_root_d = plesk_config.get('PRODUCT_ROOT_D')
    except KeyError as ex:
        log.debug("No PRODUCT_ROOT_D in psa.conf: %s", ex)
    return product_root_d


def cfgmon_archive(filepath):
    """ Save a copy of current version of file at filepath into cfgmon archive. """
    log.debug("Saving archive copy of '%s' via cfgmon", filepath)

    product_root_d = get_product_root_d()
    try:
        cfgmon = os.path.join(product_root_d, "admin", "sbin", "cfgmon")
        subprocess.check_call([cfgmon, "--update", filepath])
    except subprocess.CalledProcessError as ex:
        log.warning("Failed to archive previous version of configuration file: %s", ex)

def f2b_watchdog_reconfigure(enable):
    """ Enable/disable fail2ban service monitoring in watchdog. """
    product_root_d = get_product_root_d()
    wd_path = os.path.join(product_root_d, "admin", "bin", "modules", "watchdog", "wd")
    if not os.path.exists(wd_path):
        return
    monit_opt = enable and "--monit-service=" or "--unmonit-service="
    try:
        subprocess.check_call([wd_path, monit_opt + "fail2ban"])
        subprocess.check_call([wd_path, "--adapt"])
    except subprocess.CalledProcessError as ex:
        log.warning("Failed to configure fail2ban - watchdog integration: %s", ex)

class Fail2BanService:
    def __init__(self):
        self.service = "fail2ban"

    def action(self, act):
        if act not in ('start', 'stop', 'restart', 'reload', 'status', 'condrestart'):
            raise ValueError("Unsupported %s service action: %s" % (self.service, act))

        if not plesk_service.action(self.service, act, emulate=act in ('condrestart',)):
            raise RuntimeError("Failed to %s %s service" % (act, self.service))

        log.debug("%s service %s succeeded" % (self.service, act))

    def status(self):
        return plesk_service.action(self.service, 'status')

    def enable(self):
        plesk_service.register(self.service)
        log.debug("%s service registered successfully" % self.service)

    def disable(self):
        plesk_service.deregister(self.service)
        log.debug("%s service deregistered successfully" % self.service)

    def is_enabled(self):
        return plesk_service.is_registered(self.service)


class Fail2BanConfigManager:
    BASEDIR = '/etc/fail2ban'
    SOURCE_OPTION = '__source__'

    def __init__(self, configs_in, config_out=None, track_source=True):
        self.cfgfiles_in = configs_in
        self.cfgfile_out = config_out
        self.changed = False

        self.configs = ConfigParser.RawConfigParser()
        self.out_config = ConfigParser.RawConfigParser()

        for conf in self.cfgfiles_in:
            self.configs.read(os.path.join(self.BASEDIR, conf))
            if track_source:
                self._mark_new_sections_origin(self.configs, conf)

        if self.cfgfile_out is not None:
            self.out_config.read(os.path.join(self.BASEDIR, self.cfgfile_out))

    @staticmethod
    def glob_configs(subdir, ext='conf'):
        """ Returns list of config file names (with extensions) in a given subdirectory with a given extension. """
        return sorted(map(os.path.basename, glob(os.path.join(Fail2BanConfigManager.BASEDIR, subdir, '*.%s' % ext))))

    @staticmethod
    def _mark_new_sections_origin(config, origin):
        for section in config.sections():
            if not config.has_option(section, Fail2BanConfigManager.SOURCE_OPTION):
                config.set(section, Fail2BanConfigManager.SOURCE_OPTION, origin)

    def set(self, section, option, value):
        if option == Fail2BanConfigManager.SOURCE_OPTION:
            return
        if self.configs.has_option(section, option) and self.configs.get(section, option) == value:
            return

        if value is None:
            self.out_config.remove_option(section, option)
        else:
            if isinstance(value, basestring):
                value = os.linesep.join(value.splitlines()) # universal newlines
            self.out_config.set(section, option, value)
        self.changed = True
        log.debug("Successfully updated %s.%s => %s" % (section, option, value))

    def get(self, section, option=None):
        if option is None:
            return self.configs.items(section)
        elif option == "enabled":
            return self.configs.getboolean(section, option)
        else:
            return self.configs.get(section, option)

    def write(self, fileobject):
        self.configs.write(fileobject)

    def add_section(self, section_name):
        if section_name == 'DEFAULT':
            return  # just skip

        try:
            return self.out_config.add_section(section_name)
        except ConfigParser.DuplicateSectionError:
            pass    # section already exists
        else:
            self.changed = True

    def remove_section(self, section_name):
        section_existed = self.out_config.remove_section(section_name)
        self.changed = self.changed or section_existed
        return section_existed

    def sections(self):
        return self.configs.sections()

    def commit(self):
        if self.changed:
            output_filepath = os.path.join(self.BASEDIR, self.cfgfile_out)

            # Archive a copy of target file
            cfgmon_archive(output_filepath)

            # Remove empty sections
            for section in self.out_config.sections():
                if not self.out_config.items(section):
                    self.remove_section(section)

            # Write output file
            with open(output_filepath, 'w') as cfgfile:
                self.out_config.write(cfgfile)
                log.debug("Successfully updated %s" % cfgfile)

            self.changed = False
            return True
        return False


class JailsManager:
    def __init__(self):
        # We don't include jail.d/*.local configs since they override jail.local anyway (and we don't write them)
        jail_d_configs = map(lambda f: os.path.join('jail.d', f), Fail2BanConfigManager.glob_configs('jail.d'))
        self._config_files = ['jail.conf'] + jail_d_configs + ['jail.local']
        self.config = Fail2BanConfigManager(self._config_files, 'jail.local')

        self._config_files_to_pkgs = None

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, exc_traceback):
        if exc_type is not None:
            # If exception occured inside 'with' block, don't save changes and propagate the exception
            return False

        self.config.commit()
        return True

    def set(self, option, value):
        self.config.set('DEFAULT', option, value)

    def set_jail_option(self, jail_name, option, value):
        self.config.add_section(jail_name)
        self.config.set(jail_name, option, value)

    def get(self):
        return self.config.get('DEFAULT')

    def get_jails(self):
        return self.config.sections()

    def get_jail_info(self, jail_name, option = None):
        return self.config.get(jail_name, option)

    def get_jail_package(self, jail_name):
        if self._config_files_to_pkgs is None:
            packages = get_packages_by_files(map(lambda f: os.path.join(Fail2BanConfigManager.BASEDIR, f), 
                                                 self._config_files))
            self._config_files_to_pkgs = dict(zip(self._config_files, packages))
        return self._config_files_to_pkgs[ self.config.get(jail_name, Fail2BanConfigManager.SOURCE_OPTION) ]

    def remove_jail(self, jail_name):
        return self.config.remove_section(jail_name)


class SubConfigManager(object):
    SUBDIR = None

    def __init__(self, basename):
        self._basename = basename
        conf_file  = os.path.join(self.SUBDIR, '%s.conf' % basename)
        local_file = os.path.join(self.SUBDIR, '%s.local' % basename)
        self._config = Fail2BanConfigManager([conf_file, local_file], local_file, track_source=False)

    @staticmethod
    def _extract_name(filename_with_ext):
        """ Extracts 'name' from 'name.ext'. """
        return os.path.splitext(filename_with_ext)[0]

    @classmethod
    def list(cls):
        """ List configuration file names. Local-only files are listed with .local extension. """
        conf_files  = Fail2BanConfigManager.glob_configs(cls.SUBDIR, 'conf')
        local_files = Fail2BanConfigManager.glob_configs(cls.SUBDIR, 'local')
        conf_names  = map(cls._extract_name, conf_files)
        uniq_locals = filter(lambda f: cls._extract_name(f) not in conf_names, local_files)

        return sorted(conf_files + uniq_locals)

    @classmethod
    def list_packages(cls):
        """ List configuration files and packages they belong to. Returns list of (config, package) tuples.
            Here config is configuration file basename without extension.
            If package is None then corresponding config is custom or local.
        """
        configs = cls.list()
        filenames = map(lambda f: os.path.join(Fail2BanConfigManager.BASEDIR, cls.SUBDIR, f), configs)
        packages = get_packages_by_files(filenames)
        return zip(map(cls._extract_name, configs), packages)

    def write_to(self, fileobject):
        """ Write merged configuration to a fileobject. """
        self._config.write(fileobject)

    def update_from(self, fileobject):
        """ Update local configuration with changes so that merged configuration looks like one in fileobject. """
        updated = ConfigParser.RawConfigParser()
        updated.readfp(fileobject)

        for section in ['DEFAULT'] + updated.sections():
            self._config.add_section(section)
            for option, value in updated.items(section):
                self._config.set(section, option, value)
            if section in self._config.sections():
                for option, value in self._config.get(section):
                    if not updated.has_option(section, option):
                        self._config.set(section, option, None)

        return self._config.commit()

    def remove(self):
        """ Remove local modifications (only .local file is removed). """
        local_file = os.path.join(Fail2BanConfigManager.BASEDIR, self.SUBDIR, '%s.local' % self._basename)
        cfgmon_archive(local_file)
        try:
            os.unlink(local_file)
        except OSError as ex:
            log.debug("Configuration file removal failed: %s", ex)
            if ex.errno != errno.ENOENT:
                raise


class FilterManager(SubConfigManager):
    SUBDIR = 'filter.d'

    @classmethod
    def inspect_filters(cls):
        """ Deep inspection of all filters. Returns list of (config, package, is_complete) tuples. 
            Only complete filters may be used as jail 'filter' option value. Incomplete filters are
            usually parts of other filters. They are normally included via [INCLUDES] section.
        """
        array = ConfigurationArray()
        cfg_pkg_list = cls.list_packages()
        return map(lambda (cfg, pkg): (cfg, pkg, array.filter(cfg).is_complete()), cfg_pkg_list)


class ActionManager(SubConfigManager):
    SUBDIR = 'action.d'

    @classmethod
    def inspect_actions(cls):
        """ Deep inspection of all actions. Returns list of (config, package, params) tuples. 
            If action is not complete (that is, it cannot be used in jail 'action' option value)
            then params is None, otherwise it is a list of default parameters for the action.
            In the latter case params is a list of (name, value) tuples, it may also be empty.
        """
        # TODO write doc
        array = ConfigurationArray()
        cfg_pkg_list = cls.list_packages()
        return map(lambda (cfg, pkg): (cfg, pkg, array.action(cfg).default_parameters() 
                                                 if array.action(cfg).is_complete()
                                                 else None),
                   cfg_pkg_list)


def _execute_fail2ban_command(command):
    c = Fail2BanConfigManager(['fail2ban.conf', 'fail2ban.local'])
    socket_path = c.get('Definition', 'socket')
    log.debug("Running %s via %s" % (command, socket_path))

    eoc = '<F2B_END_COMMAND>'
    chunk_size = 4096
    s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
    s.connect(socket_path)

    data = pickle.dumps(map(str, command), pickle.HIGHEST_PROTOCOL) + eoc

    s.sendall(data)

    ret = ''
    while True:
        chunk = s.recv(chunk_size)
        if not chunk:
            break
        ret = ret + chunk

    s.close() # Will also be called on s garbage-collection, e.g. if exception happened
    # response is either (0, string or command-defined list/dict) or (1, exception object)
    response = pickle.loads(ret[:ret.rfind(eoc)])
    log.debug("Command %s, retcode %s, response %s" % (command, response[0], response[1]))

    if response[0] != 0:
        raise RuntimeError("Command %s failed with error %s" % (command, repr(response[1])))

    return response[1]


class F2bmngApp(OptionMatcher):
    """ Main f2bmng application class.
        
        Parses command line options via optmatch (see http://coderazzi.net/python/optmatch/).
    """
    _OPTIONS_HELP = {}
    _OPTION_VAR_NAMES = {
        'get-jail':                 'JAIL_NAME',
        'set-jail':                 'JAIL_NAME',
        'get-filter':               'FILTER_NAME',
        'set-filter':               'FILTER_NAME',
        'get-action':               'ACTION_NAME',
        'set-action':               'ACTION_NAME',
        'source-basedir':           'BASEDIR',
        'source-version':           'VERSION',
    }

    def __init__(self):
        super(F2bmngApp, self).__init__(optionsHelp=self._OPTIONS_HELP, 
                                        optionVarNames=self._OPTION_VAR_NAMES)

    def main(self, argv):
        log.debug("argv = %s", argv)
        self.process(argv, handleUsageProblems=False)

    def printHelp(self):
        """ Show the help message. """
        # Overrides method from OptionMatcher.
        print(self.getUsage().getUsageString(width=120, column=32))

    def _check_is_server_alive(self):
        """ Checks whether service is up and responsive. Returns True in this case. Prints warning if not. """
        ping_response = None
        try:
            ping_response = _execute_fail2ban_command(['ping'])
        except Exception as ex:
            log.debug("Ping command resulted in exception: %s", ex)

        if ping_response == 'pong':
            return True
        else:
            log.warning("Fail2ban server is down. Configuration changes will be applied upon start.")
            return False

    def _reload_service(self):
        """ Reload fail2ban service. Raises exception if failed. Errors and warnings from fail2ban go to stderr. """
        # We don't use Fail2BanService().action('reload') since init script on Debian doesn't report proper error code
        if self._check_is_server_alive():
            subprocess.check_call(["/usr/bin/fail2ban-client", "reload"])

    def _reload_jails(self, jails):
        """ Reload specified jails. Goes on if failed, but reports all errors and raises exception at the end. """
        if not self._check_is_server_alive():
            return

        try:
            subprocess.check_call(["/usr/bin/fail2ban-client", "reload"])
        except subprocess.CalledProcessError as ex:
            log.error('%s', ex)
            raise Exception("Failed to reload following jails due to errors in configuration")

    def _reload_jail(self, jail_name):
        """ Reload specified jail configuration. Raises exception if failed. Errors and warnings go to stderr. """
        self._reload_jails([jail_name])

    # Service management

    @optmatcher
    def do_enable(self, enableFlag):
        """ Enable fail2ban service. """
        service = Fail2BanService()
        service.enable()
        service.action('start')
        f2b_watchdog_reconfigure(enable=True)

    @optmatcher
    def do_disable(self, disableFlag):
        """ Disable fail2ban service. """
        f2b_watchdog_reconfigure(enable=False)
        service = Fail2BanService()
        service.action('stop')
        service.disable()

    @optmatcher
    def do_start(self, startFlag):
        """ Start fail2ban service. """
        Fail2BanService().action('start')

    @optmatcher
    def do_stop(self, stopFlag):
        """ Stop fail2ban service. """
        Fail2BanService().action('stop')

    @optmatcher
    def do_restart(self, restartFlag):
        """ Restart fail2ban service. """
        Fail2BanService().action('restart')

    @optmatcher
    def do_reload(self, reloadFlag):
        """ Reload fail2ban service. """
        self._reload_service()

    @optmatcher
    def do_status(self, statusFlag):
        """ Check whether fail2ban service is running. """
        print(Fail2BanService().status() and "is running" or "is stopped")

    @optmatcher
    def do_is_enabled(self, isEnabledFlag):
        """ Check whether fail2ban service is enabled. """
        print(Fail2BanService().is_enabled() and "is enabled" or "is disabled")

    # Default options management

    @optmatcher
    def do_get_options(self, getOptionsFlag):
        """ Get fail2ban options. Output is JSON object. """
        with JailsManager() as c:
            out_options = dict(c.get())
        json.dump(out_options, sys.stdout)

    @optmatcher
    def do_set_options(self, setOptionsFlag):
        """ Set fail2ban options. Input is JSON object. """
        options = json.load(sys.stdin)
        with JailsManager() as c:
            for k, v in options.iteritems():
                c.set(k, v)
        self._reload_service()

    # Bans management

    @optmatcher
    def do_get_banned_ips(self, getBannedIpsFlag):
        """ Get currently banned ip adresses. Output is JSON list of [ip, jail] entries. """
        try:
            r = _execute_fail2ban_command(['status'])
        except IOError, socket.SocketError:
            return json.dump([], sys.stdout)

        active_jails = []

        for j in r:
            if j[0] == 'Jail list' and j[1]:
                active_jails = j[1].split(", ")

        ip_list = []
        for j in active_jails:
            r = _execute_fail2ban_command(['status', j])
            for ip in r[1][1][2][1]:
                ip_list.append([ip, j])

        json.dump(ip_list, sys.stdout)

    @optmatcher
    def do_unban(self, unbanFlag):
        """ Unban specified ip adresses. Input is JSON list of [ip, jail] entries. """
        data = json.load(sys.stdin)
        for ip, jail in data:
           _execute_fail2ban_command(['set', str(jail), 'unbanip', str(ip)])

    # Jails management

    @optmatcher
    def do_get_jails_list(self, getJailsListFlag):
        """ List fail2ban jails. Output is JSON list of [jail, is enabled, package] entries. """
        jails_info = []
        with JailsManager() as c:
            for j in c.get_jails():
                enabled = c.get_jail_info(j, 'enabled')
                package = c.get_jail_package(j)
                log.debug("Jail %s, %s, %s", 
                          j, 
                          "enabled" if enabled else "disabled", 
                          package or "<custom>")
                jails_info.append([j, enabled, package])
        json.dump(jails_info, sys.stdout)

    @optmatcher
    def do_get_jail(self, getJailOption):
        """ Fetch jail information. Includes DEFAULT options. Output is JSON list of [option, value] entries. """
        jail_name = getJailOption
        with JailsManager() as c:
            return json.dump(c.get_jail_info(jail_name), sys.stdout)

    @optmatcher
    def do_set_jail(self, setJailOption):
        """ Update jail information. Input is JSON list of [option, value] entries. """
        jail_name = setJailOption
        options = json.load(sys.stdin)
        with JailsManager() as c:
            for k, v in options:
                c.set_jail_option(jail_name, k, v)
        self._reload_jail(jail_name)

    def _switch_jails_state(self, jails, enable=True):
        """ Enabled/disable jails. """
        with JailsManager() as c:
            for jail in jails:
                c.set_jail_option(jail, "enabled", str(bool(enable)).lower())
        self._reload_jails(jails)

    @optmatcher
    def do_enable_jails(self, enableJailsFlag, *jails):
        """ Enable jails specified as arguments. """
        self._switch_jails_state(jails, True)

    @optmatcher
    def do_disable_jails(self, disableJailsFlag, *jails):
        """ Disable jails specified as arguments. """
        self._switch_jails_state(jails, False)

    @optmatcher
    def do_remove_jails(self, removeJailsFlag, *jails):
        """ Remove jails specified as arguments. Only local modifications are removed. """
        with JailsManager() as c:
            for jail in jails:
                c.remove_jail(jail)
        self._reload_service()

    # Filters

    @optmatcher
    def do_get_filters_list(self, getFiltersListFlag, inspectFlag=None):
        """ List fail2ban filters. Output is JSON list of [filter, package] entries. 
            With --inspect entries have [filter, package, is-complete] form instead.
        """
        if not inspectFlag:
            json.dump(FilterManager.list_packages(), sys.stdout)
        else:
            json.dump(FilterManager.inspect_filters(), sys.stdout)

    @optmatcher
    def do_remove_filters(self, removeFiltersFlag, *filters):
        """ Remove filters specified as arguments. Only local modifications are removed. """
        for filter_name in filters:
            FilterManager(filter_name).remove()
        self._reload_service()

    @optmatcher
    def do_get_filter(self, getFilterOption):
        """ Fetch filter configuration. Output is merged configuration content. """
        filter_name = getFilterOption
        FilterManager(filter_name).write_to(sys.stdout)

    @optmatcher
    def do_set_filter(self, setFilterOption):
        """ Update filter configuration. Input is target configuration file content. """
        filter_name = setFilterOption
        FilterManager(filter_name).update_from(sys.stdin)
        self._reload_service()

    # Actions

    @optmatcher
    def do_get_actions_list(self, getActionsListFlag, inspectFlag=None):
        """ List fail2ban actions. Output is JSON list of [action, package] entries. 
            With --inspect entries have [filter, package, default-params] form instead.
            default-params is a list of [name, value] entries if action is complete,
            otherwise it is null. default-params list may be empty as well.
        """
        if not inspectFlag:
            json.dump(ActionManager.list_packages(), sys.stdout)
        else:
            json.dump(ActionManager.inspect_actions(), sys.stdout)

    @optmatcher
    def do_remove_actions(self, removeActionsFlag, *actions):
        """ Remove actions specified as arguments. Only local modifications are removed. """
        for action_name in actions:
            ActionManager(action_name).remove()
        self._reload_service()

    @optmatcher
    def do_get_action(self, getActionOption):
        """ Fetch action configuration. Output is merged configuration content. """
        action_name = getActionOption
        ActionManager(action_name).write_to(sys.stdout)

    @optmatcher
    def do_set_action(self, setActionOption):
        """ Update action configuration. Input is target configuration file content. """
        action_name = setActionOption
        ActionManager(action_name).update_from(sys.stdin)
        self._reload_service()

    @optmatcher
    def do_add_log(self, addLogFlag, jailName, logPath):
        """ Add log file to jail for monitoring. """
        _execute_fail2ban_command(['set', jailName, 'addlogpath', logPath])

    @optmatcher
    def do_del_log(self, delLogFlag, jailName, logPath):
        """ Remove log file from monitoring from specified jail. """
        _execute_fail2ban_command(['set', jailName, 'dellogpath', logPath])

    # Upgrade

    @optmatcher
    def do_upgrade(self, upgradeConfigurationFlag, sourceBasedirOption, sourceVersionOption):
        """ Upgrade fail2ban configuration. Current configuration must not have customizations. 
            Source configuration should be pointed by --source-basedir and its version 
            specified via --source-version.
        """
        if not os.path.isdir(sourceBasedirOption):
            raise RuntimeError("Source basedir '%s' doesn't exist" % sourceBasedirOption)
        if not os.path.isdir(ConfigurationArray.DEFAULT_BASEDIR):
            raise RuntimeError("Target basedir '%s' doesn't exist" % ConfigurationArray.DEFAULT_BASEDIR)
        if LooseVersion(sourceVersionOption) < LooseVersion('0.7'):
            raise ValueError("Source version %s is too old. "
                             "Only configurations starting from version 0.7 may be upgraded" % sourceVersionOption)

        target_array = ConfigurationArray()
        source_array = ConfigurationArray(sourceBasedirOption)

        target_array.merge_from(source_array)

        Fail2BanService().action('condrestart')

    # Logs

    @optmatcher
    def do_get_logs_list(self, getLogsListFlag):
        """ List fail2ban log files, including rotated ones. Output is JSON list, empty if no dedicated logs. """
        c = Fail2BanConfigManager(['fail2ban.conf', 'fail2ban.local'])
        log_path = c.get('Definition', 'logtarget')
        logs_info = []
        if log_path.startswith('/'):
            logs_info.append(log_path)
            logs_info += glob(log_path + '.*')  # rotated logs (*.1, *.2.gz, and the likes)
        json.dump(sorted(logs_info), sys.stdout)


def main():
    """ f2bmng main entry point for command-line execution. """
    try:
        F2bmngApp().main(sys.argv)
    except SystemExit:
        raise
    except UsageException as ex:
        sys.stderr.write('%s\n' % ex)
        sys.exit(1)
    except Exception as ex:
        log.error('%s', ex)
        log.debug('This exception happened at:', exc_info=sys.exc_info())
        sys.exit(1)

if __name__ == '__main__':
    main()

# vim: ts=4 sts=4 sw=4 et :
