saltstack/salt

View on GitHub
salt/modules/logadm.py

Summary

Maintainability
C
1 day
Test Coverage
# -*- coding: utf-8 -*-
'''
Module for managing Solaris logadm based log rotations.
'''
from __future__ import absolute_import, print_function, unicode_literals

# Import python libs
import logging
import shlex
try:
    from shlex import quote as _quote_args  # pylint: disable=E0611
except ImportError:
    from pipes import quote as _quote_args

# Import salt libs
from salt.ext import six
import salt.utils.args
import salt.utils.decorators as decorators
import salt.utils.files
import salt.utils.stringutils

log = logging.getLogger(__name__)
default_conf = '/etc/logadm.conf'
option_toggles = {
    '-c': 'copy',
    '-l': 'localtime',
    '-N': 'skip_missing',
}
option_flags = {
    '-A': 'age',
    '-C': 'count',
    '-a': 'post_command',
    '-b': 'pre_command',
    '-e': 'mail_addr',
    '-E': 'expire_command',
    '-g': 'group',
    '-m': 'mode',
    '-M': 'rename_command',
    '-o': 'owner',
    '-p': 'period',
    '-P': 'timestmp',
    '-R': 'old_created_command',
    '-s': 'size',
    '-S': 'max_size',
    '-t': 'template',
    '-T': 'old_pattern',
    '-w': 'entryname',
    '-z': 'compress_count',
}


def __virtual__():
    '''
    Only work on Solaris based systems
    '''
    if 'Solaris' in __grains__.get('os_family'):
        return True
    return (False, 'The logadm execution module cannot be loaded: only available on Solaris.')


def _arg2opt(arg):
    '''
    Turn a pass argument into the correct option
    '''
    res = [o for o, a in option_toggles.items() if a == arg]
    res += [o for o, a in option_flags.items() if a == arg]
    return res[0] if res else None


def _parse_conf(conf_file=default_conf):
    '''
    Parse a logadm configuration file.
    '''
    ret = {}
    with salt.utils.files.fopen(conf_file, 'r') as ifile:
        for line in ifile:
            line = salt.utils.stringutils.to_unicode(line).strip()
            if not line:
                continue
            if line.startswith('#'):
                continue
            splitline = line.split(' ', 1)
            ret[splitline[0]] = splitline[1]
    return ret


def _parse_options(entry, options, include_unset=True):
    '''
    Parse a logadm options string
    '''
    log_cfg = {}
    options = shlex.split(options)
    if not options:
        return None

    ## identifier is entry or log?
    if entry.startswith('/'):
        log_cfg['log_file'] = entry
    else:
        log_cfg['entryname'] = entry

    ## parse options
    # NOTE: we loop over the options because values may exist multiple times
    index = 0
    while index < len(options):
        # log file
        if index in [0, (len(options)-1)] and options[index].startswith('/'):
            log_cfg['log_file'] = options[index]

        # check if toggle option
        elif options[index] in option_toggles:
            log_cfg[option_toggles[options[index]]] = True

        # check if flag option
        elif options[index] in option_flags and (index+1) <= len(options):
            log_cfg[option_flags[options[index]]] = int(options[index+1]) if options[index+1].isdigit() else options[index+1]
            index += 1

        # unknown options
        else:
            if 'additional_options' not in log_cfg:
                log_cfg['additional_options'] = []
            if ' ' in options[index]:
                log_cfg['dditional_options'] = "'{}'".format(options[index])
            else:
                log_cfg['additional_options'].append(options[index])

        index += 1

    ## turn additional_options into string
    if 'additional_options' in log_cfg:
        log_cfg['additional_options'] = " ".join(log_cfg['additional_options'])

    ## ensure we have a log_file
    # NOTE: logadm assumes logname is a file if no log_file is given
    if 'log_file' not in log_cfg and 'entryname' in log_cfg:
        log_cfg['log_file'] = log_cfg['entryname']
        del log_cfg['entryname']

    ## include unset
    if include_unset:
        # toggle optioons
        for name in option_toggles.values():
            if name not in log_cfg:
                log_cfg[name] = False

        # flag options
        for name in option_flags.values():
            if name not in log_cfg:
                log_cfg[name] = None

    return log_cfg


def show_conf(conf_file=default_conf, name=None):
    '''
    Show configuration

    conf_file : string
        path to logadm.conf, defaults to /etc/logadm.conf
    name : string
        optional show only a single entry

    CLI Example:

    .. code-block:: bash

        salt '*' logadm.show_conf
        salt '*' logadm.show_conf name=/var/log/syslog
    '''
    cfg = _parse_conf(conf_file)

    # filter
    if name and name in cfg:
        return {name: cfg[name]}
    elif name:
        return {name: 'not found in {}'.format(conf_file)}
    else:
        return cfg


def list_conf(conf_file=default_conf, log_file=None, include_unset=False):
    '''
    Show parsed configuration

    .. versionadded:: 2018.3.0

    conf_file : string
        path to logadm.conf, defaults to /etc/logadm.conf
    log_file : string
        optional show only one log file
    include_unset : boolean
        include unset flags in output

    CLI Example:

    .. code-block:: bash

        salt '*' logadm.list_conf
        salt '*' logadm.list_conf log=/var/log/syslog
        salt '*' logadm.list_conf include_unset=False
    '''
    cfg = _parse_conf(conf_file)
    cfg_parsed = {}

    ## parse all options
    for entry in cfg:
        log_cfg = _parse_options(entry, cfg[entry], include_unset)
        cfg_parsed[log_cfg['log_file'] if 'log_file' in log_cfg else log_cfg['entryname']] = log_cfg

    ## filter
    if log_file and log_file in cfg_parsed:
        return {log_file: cfg_parsed[log_file]}
    elif log_file:
        return {log_file: 'not found in {}'.format(conf_file)}
    else:
        return cfg_parsed


@decorators.memoize
def show_args():
    '''
    Show which arguments map to which flags and options.

    .. versionadded:: 2018.3.0

    CLI Example:

    .. code-block:: bash

        salt '*' logadm.show_args
    '''
    mapping = {'flags': {}, 'options': {}}
    for flag, arg in option_toggles.items():
        mapping['flags'][flag] = arg
    for option, arg in option_flags.items():
        mapping['options'][option] = arg

    return mapping


def rotate(name, pattern=None, conf_file=default_conf, **kwargs):
    '''
    Set up pattern for logging.

    name : string
        alias for entryname
    pattern : string
        alias for log_file
    conf_file : string
        optional path to alternative configuration file
    kwargs : boolean|string|int
        optional additional flags and parameters

    .. note::
        ``name`` and ``pattern`` were kept for backwards compatibility reasons.

        ``name`` is an alias for the ``entryname`` argument, ``pattern`` is an alias
        for ``log_file``. These aliases will only be used if the ``entryname`` and
        ``log_file`` arguments are not passed.

        For a full list of arguments see ```logadm.show_args```.

    CLI Example:

    .. code-block:: bash

        salt '*' logadm.rotate myapplog pattern='/var/log/myapp/*.log' count=7
        salt '*' logadm.rotate myapplog log_file='/var/log/myapp/*.log' count=4 owner=myappd mode='0700'

    '''
    ## cleanup kwargs
    kwargs = salt.utils.args.clean_kwargs(**kwargs)

    ## inject name into kwargs
    if 'entryname' not in kwargs and name and not name.startswith('/'):
        kwargs['entryname'] = name

    ## inject pattern into kwargs
    if 'log_file' not in kwargs:
        if pattern and pattern.startswith('/'):
            kwargs['log_file'] = pattern
        # NOTE: for backwards compatibility check if name is a path
        elif name and name.startswith('/'):
            kwargs['log_file'] = name

    ## build command
    log.debug("logadm.rotate - kwargs: %s", kwargs)
    command = "logadm -f {}".format(conf_file)
    for arg, val in kwargs.items():
        if arg in option_toggles.values() and val:
            command = "{} {}".format(
                command,
                _arg2opt(arg),
            )
        elif arg in option_flags.values():
            command = "{} {} {}".format(
                command,
                _arg2opt(arg),
                _quote_args(six.text_type(val))
            )
        elif arg != 'log_file':
            log.warning("Unknown argument %s, don't know how to map this!", arg)
    if 'log_file' in kwargs:
        # NOTE: except from ```man logadm```
        #   If no log file name is provided on a logadm command line, the entry
        #   name is assumed to be the same as the log file name. For example,
        #   the following two lines achieve the same thing, keeping two copies
        #   of rotated log files:
        #
        #     % logadm -C2 -w mylog /my/really/long/log/file/name
        #     % logadm -C2 -w /my/really/long/log/file/name
        if 'entryname' not in kwargs:
            command = "{} -w {}".format(command, _quote_args(kwargs['log_file']))
        else:
            command = "{} {}".format(command, _quote_args(kwargs['log_file']))

    log.debug("logadm.rotate - command: %s", command)
    result = __salt__['cmd.run_all'](command, python_shell=False)
    if result['retcode'] != 0:
        return dict(Error='Failed in adding log', Output=result['stderr'])

    return dict(Result='Success')


def remove(name, conf_file=default_conf):
    '''
    Remove log pattern from logadm

    CLI Example:

    .. code-block:: bash

      salt '*' logadm.remove myapplog
    '''
    command = "logadm -f {0} -r {1}".format(conf_file, name)
    result = __salt__['cmd.run_all'](command, python_shell=False)
    if result['retcode'] != 0:
        return dict(
            Error='Failure in removing log. Possibly already removed?',
            Output=result['stderr']
        )
    return dict(Result='Success')