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