1"""
2Module for managing Solaris logadm based log rotations.
3"""
4
5import logging
6import shlex
7
8import salt.utils.args
9import salt.utils.decorators as decorators
10import salt.utils.files
11import salt.utils.stringutils
12
13try:
14    from shlex import quote as _quote_args  # pylint: disable=E0611
15except ImportError:
16    from pipes import quote as _quote_args
17
18
19log = logging.getLogger(__name__)
20default_conf = "/etc/logadm.conf"
21option_toggles = {
22    "-c": "copy",
23    "-l": "localtime",
24    "-N": "skip_missing",
25}
26option_flags = {
27    "-A": "age",
28    "-C": "count",
29    "-a": "post_command",
30    "-b": "pre_command",
31    "-e": "mail_addr",
32    "-E": "expire_command",
33    "-g": "group",
34    "-m": "mode",
35    "-M": "rename_command",
36    "-o": "owner",
37    "-p": "period",
38    "-P": "timestmp",
39    "-R": "old_created_command",
40    "-s": "size",
41    "-S": "max_size",
42    "-t": "template",
43    "-T": "old_pattern",
44    "-w": "entryname",
45    "-z": "compress_count",
46}
47
48
49def __virtual__():
50    """
51    Only work on Solaris based systems
52    """
53    if "Solaris" in __grains__["os_family"]:
54        return True
55    return (
56        False,
57        "The logadm execution module cannot be loaded: only available on Solaris.",
58    )
59
60
61def _arg2opt(arg):
62    """
63    Turn a pass argument into the correct option
64    """
65    res = [o for o, a in option_toggles.items() if a == arg]
66    res += [o for o, a in option_flags.items() if a == arg]
67    return res[0] if res else None
68
69
70def _parse_conf(conf_file=default_conf):
71    """
72    Parse a logadm configuration file.
73    """
74    ret = {}
75    with salt.utils.files.fopen(conf_file, "r") as ifile:
76        for line in ifile:
77            line = salt.utils.stringutils.to_unicode(line).strip()
78            if not line:
79                continue
80            if line.startswith("#"):
81                continue
82            splitline = line.split(" ", 1)
83            ret[splitline[0]] = splitline[1]
84    return ret
85
86
87def _parse_options(entry, options, include_unset=True):
88    """
89    Parse a logadm options string
90    """
91    log_cfg = {}
92    options = shlex.split(options)
93    if not options:
94        return None
95
96    ## identifier is entry or log?
97    if entry.startswith("/"):
98        log_cfg["log_file"] = entry
99    else:
100        log_cfg["entryname"] = entry
101
102    ## parse options
103    # NOTE: we loop over the options because values may exist multiple times
104    index = 0
105    while index < len(options):
106        # log file
107        if index in [0, (len(options) - 1)] and options[index].startswith("/"):
108            log_cfg["log_file"] = options[index]
109
110        # check if toggle option
111        elif options[index] in option_toggles:
112            log_cfg[option_toggles[options[index]]] = True
113
114        # check if flag option
115        elif options[index] in option_flags and (index + 1) <= len(options):
116            log_cfg[option_flags[options[index]]] = (
117                int(options[index + 1])
118                if options[index + 1].isdigit()
119                else options[index + 1]
120            )
121            index += 1
122
123        # unknown options
124        else:
125            if "additional_options" not in log_cfg:
126                log_cfg["additional_options"] = []
127            if " " in options[index]:
128                log_cfg["dditional_options"] = "'{}'".format(options[index])
129            else:
130                log_cfg["additional_options"].append(options[index])
131
132        index += 1
133
134    ## turn additional_options into string
135    if "additional_options" in log_cfg:
136        log_cfg["additional_options"] = " ".join(log_cfg["additional_options"])
137
138    ## ensure we have a log_file
139    # NOTE: logadm assumes logname is a file if no log_file is given
140    if "log_file" not in log_cfg and "entryname" in log_cfg:
141        log_cfg["log_file"] = log_cfg["entryname"]
142        del log_cfg["entryname"]
143
144    ## include unset
145    if include_unset:
146        # toggle optioons
147        for name in option_toggles.values():
148            if name not in log_cfg:
149                log_cfg[name] = False
150
151        # flag options
152        for name in option_flags.values():
153            if name not in log_cfg:
154                log_cfg[name] = None
155
156    return log_cfg
157
158
159def show_conf(conf_file=default_conf, name=None):
160    """
161    Show configuration
162
163    conf_file : string
164        path to logadm.conf, defaults to /etc/logadm.conf
165    name : string
166        optional show only a single entry
167
168    CLI Example:
169
170    .. code-block:: bash
171
172        salt '*' logadm.show_conf
173        salt '*' logadm.show_conf name=/var/log/syslog
174    """
175    cfg = _parse_conf(conf_file)
176
177    # filter
178    if name and name in cfg:
179        return {name: cfg[name]}
180    elif name:
181        return {name: "not found in {}".format(conf_file)}
182    else:
183        return cfg
184
185
186def list_conf(conf_file=default_conf, log_file=None, include_unset=False):
187    """
188    Show parsed configuration
189
190    .. versionadded:: 2018.3.0
191
192    conf_file : string
193        path to logadm.conf, defaults to /etc/logadm.conf
194    log_file : string
195        optional show only one log file
196    include_unset : boolean
197        include unset flags in output
198
199    CLI Example:
200
201    .. code-block:: bash
202
203        salt '*' logadm.list_conf
204        salt '*' logadm.list_conf log=/var/log/syslog
205        salt '*' logadm.list_conf include_unset=False
206    """
207    cfg = _parse_conf(conf_file)
208    cfg_parsed = {}
209
210    ## parse all options
211    for entry in cfg:
212        log_cfg = _parse_options(entry, cfg[entry], include_unset)
213        cfg_parsed[
214            log_cfg["log_file"] if "log_file" in log_cfg else log_cfg["entryname"]
215        ] = log_cfg
216
217    ## filter
218    if log_file and log_file in cfg_parsed:
219        return {log_file: cfg_parsed[log_file]}
220    elif log_file:
221        return {log_file: "not found in {}".format(conf_file)}
222    else:
223        return cfg_parsed
224
225
226@decorators.memoize
227def show_args():
228    """
229    Show which arguments map to which flags and options.
230
231    .. versionadded:: 2018.3.0
232
233    CLI Example:
234
235    .. code-block:: bash
236
237        salt '*' logadm.show_args
238    """
239    mapping = {"flags": {}, "options": {}}
240    for flag, arg in option_toggles.items():
241        mapping["flags"][flag] = arg
242    for option, arg in option_flags.items():
243        mapping["options"][option] = arg
244
245    return mapping
246
247
248def rotate(name, pattern=None, conf_file=default_conf, **kwargs):
249    """
250    Set up pattern for logging.
251
252    name : string
253        alias for entryname
254    pattern : string
255        alias for log_file
256    conf_file : string
257        optional path to alternative configuration file
258    kwargs : boolean|string|int
259        optional additional flags and parameters
260
261    .. note::
262        ``name`` and ``pattern`` were kept for backwards compatibility reasons.
263
264        ``name`` is an alias for the ``entryname`` argument, ``pattern`` is an alias
265        for ``log_file``. These aliases will only be used if the ``entryname`` and
266        ``log_file`` arguments are not passed.
267
268        For a full list of arguments see ```logadm.show_args```.
269
270    CLI Example:
271
272    .. code-block:: bash
273
274        salt '*' logadm.rotate myapplog pattern='/var/log/myapp/*.log' count=7
275        salt '*' logadm.rotate myapplog log_file='/var/log/myapp/*.log' count=4 owner=myappd mode='0700'
276
277    """
278    ## cleanup kwargs
279    kwargs = salt.utils.args.clean_kwargs(**kwargs)
280
281    ## inject name into kwargs
282    if "entryname" not in kwargs and name and not name.startswith("/"):
283        kwargs["entryname"] = name
284
285    ## inject pattern into kwargs
286    if "log_file" not in kwargs:
287        if pattern and pattern.startswith("/"):
288            kwargs["log_file"] = pattern
289        # NOTE: for backwards compatibility check if name is a path
290        elif name and name.startswith("/"):
291            kwargs["log_file"] = name
292
293    ## build command
294    log.debug("logadm.rotate - kwargs: %s", kwargs)
295    command = "logadm -f {}".format(conf_file)
296    for arg, val in kwargs.items():
297        if arg in option_toggles.values() and val:
298            command = "{} {}".format(
299                command,
300                _arg2opt(arg),
301            )
302        elif arg in option_flags.values():
303            command = "{} {} {}".format(command, _arg2opt(arg), _quote_args(str(val)))
304        elif arg != "log_file":
305            log.warning("Unknown argument %s, don't know how to map this!", arg)
306    if "log_file" in kwargs:
307        # NOTE: except from ```man logadm```
308        #   If no log file name is provided on a logadm command line, the entry
309        #   name is assumed to be the same as the log file name. For example,
310        #   the following two lines achieve the same thing, keeping two copies
311        #   of rotated log files:
312        #
313        #     % logadm -C2 -w mylog /my/really/long/log/file/name
314        #     % logadm -C2 -w /my/really/long/log/file/name
315        if "entryname" not in kwargs:
316            command = "{} -w {}".format(command, _quote_args(kwargs["log_file"]))
317        else:
318            command = "{} {}".format(command, _quote_args(kwargs["log_file"]))
319
320    log.debug("logadm.rotate - command: %s", command)
321    result = __salt__["cmd.run_all"](command, python_shell=False)
322    if result["retcode"] != 0:
323        return dict(Error="Failed in adding log", Output=result["stderr"])
324
325    return dict(Result="Success")
326
327
328def remove(name, conf_file=default_conf):
329    """
330    Remove log pattern from logadm
331
332    CLI Example:
333
334    .. code-block:: bash
335
336      salt '*' logadm.remove myapplog
337    """
338    command = "logadm -f {} -r {}".format(conf_file, name)
339    result = __salt__["cmd.run_all"](command, python_shell=False)
340    if result["retcode"] != 0:
341        return dict(
342            Error="Failure in removing log. Possibly already removed?",
343            Output=result["stderr"],
344        )
345    return dict(Result="Success")
346