1
0
Fork 0
mirror of https://github.com/ansible-collections/community.general.git synced 2026-02-04 07:51:50 +00:00
community.general/plugins/modules/logrotate.py
Александр Габидуллин 64bb002f77 fix module sanity test
2026-01-29 17:19:28 +04:00

930 lines
32 KiB
Python

#!/usr/bin/python
# Copyright (c) 2026 Aleksandr Gabidullin <qualittv@gmail.com>
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later
from __future__ import annotations
DOCUMENTATION = r"""
module: logrotate
version_added: 12.3.0
short_description: Manage logrotate configurations
description:
- Manage logrotate configuration files and settings.
- Create, update, or remove logrotate configurations for applications and services.
author: "Aleksandr Gabidullin (@a-gabidullin)"
requirements:
- logrotate >= 3.8.0
attributes:
check_mode:
support: full
diff_mode:
support: full
options:
name:
description:
- Name of the logrotate configuration.
- This creates a file in O(config_dir) with this name.
type: str
required: true
aliases: [config_name]
state:
description:
- Whether the configuration should be present or absent.
type: str
choices: [present, absent]
config_dir:
description:
- Directory where logrotate configurations are stored.
- Default is V(/etc/logrotate.d) for system-wide configurations.
- Use V(~/.logrotate.d) for user-specific configurations.
- This directory must exist before using the module.
type: path
paths:
description:
- List of log file paths or patterns to rotate.
- Can include wildcards (for example V(/var/log/app/*.log)).
- Required when creating a new configuration (O(state=present) and config file does not exist).
- Optional when modifying existing configuration (for example to enable/disable).
type: list
elements: path
rotation_period:
description:
- How often to rotate the logs.
- If not specified, existing value be preserved when modifying configuration.
- When creating new configuration, logrotate default (daily) be used if not specified.
type: str
choices: [daily, weekly, monthly, yearly]
rotate_count:
description:
- Number of rotated log files to keep.
- Set to V(0) to disable rotation (keep only current log).
- Set to V(-1) to keep all rotated logs (not recommended).
type: int
compress:
description:
- Compress rotated log files.
type: bool
compressoptions:
description:
- Options to pass to compression program.
- For gzip, use V(-9) for best compression, V(-1) for fastest.
- For xz, use V(-9) for best compression.
type: str
compression_method:
description:
- Compression method to use.
- Requires logrotate 3.18.0 or later for V(xz) and V(zstd).
type: str
choices: [gzip, bzip2, xz, zstd, lzma, lz4]
delaycompress:
description:
- Postpone compression of the previous log file to the next rotation cycle.
- Useful for applications that keep writing to the old log file for some time.
type: bool
nodelaycompress:
description:
- Opposite of O(delaycompress). Ensure compression happens immediately.
- Note that in logrotate, V(nodelaycompress) is the default behavior.
type: bool
shred:
description:
- Use shred to securely delete rotated log files.
- Uses V(shred -u) to overwrite files before deleting.
type: bool
shredcycles:
description:
- Number of times to overwrite files when using O(shred=true).
type: int
missingok:
description:
- Do not issue an error if the log file is missing.
type: bool
ifempty:
description:
- Rotate the log file even if it is empty.
- Opposite of V(notifempty).
type: bool
notifempty:
description:
- Do not rotate the log file if it is empty.
- Opposite of V(ifempty).
type: bool
create:
description:
- Create new log file with specified permissions after rotation.
- Format is V(mode owner group) (for example V(0640 root adm)).
- Set to V(null) or omit to use V(nocreate).
type: str
copytruncate:
description:
- Copy the log file and then truncate it in place.
- Useful for applications that cannot be told to close their logfile.
type: bool
copy:
description:
- Copy the log file but do not truncate the original.
- Takes precedence over O(renamecopy) and O(copytruncate).
type: bool
renamecopy:
description:
- Rename and copy the log file, leaving the original in place.
type: bool
size:
description:
- Rotate log file when it grows bigger than specified size.
- Format is V(number)[k|M|G] (for example V(100M), V(1G)).
- Overrides O(rotation_period) when set.
type: str
minsize:
description:
- Rotate log file only if it has grown bigger than specified size.
- Format is V(number)[k|M|G] (for example V(100M), V(1G)).
- Used with time-based rotation to avoid rotating too small files.
type: str
maxsize:
description:
- Rotate log file when it grows bigger than specified size, but at most once per O(rotation_period).
- Format is V(number)[k|M|G] (for example V(100M), V(1G)).
type: str
maxage:
description:
- Remove rotated logs older than specified number of days.
type: int
dateext:
description:
- Use date as extension for rotated files (YYYYMMDD instead of sequential numbers).
type: bool
dateyesterday:
description:
- Use yesterday's date for O(dateext) instead of today's date.
- Useful for rotating logs that span midnight.
type: bool
dateformat:
description:
- Format for date extension.
- Use with O(dateext=true).
- Format specifiers are V(%Y) year, V(%m) month, V(%d) day, V(%s) seconds since epoch.
type: str
sharedscripts:
description:
- Run O(prerotate) and O(postrotate) scripts only once for all matching log files.
type: bool
prerotate:
description:
- Commands to execute before rotating the log file.
- Can be a single string or list of commands.
type: list
elements: str
postrotate:
description:
- Commands to execute after rotating the log file.
- Can be a single string or list of commands.
type: list
elements: str
firstaction:
description:
- Commands to execute once before all log files that match the wildcard pattern are rotated.
type: list
elements: str
lastaction:
description:
- Commands to execute once after all log files that match the wildcard pattern are rotated.
type: list
elements: str
preremove:
description:
- Commands to execute before removing rotated log files.
type: list
elements: str
su:
description:
- Set user and group for rotated files.
- Format is V(user group) (for example V(www-data adm)).
- Set to V(null) or omit to not set user/group.
type: str
olddir:
description:
- Move rotated logs into specified directory.
type: path
createolddir:
description:
- Create O(olddir) directory if it does not exist.
type: bool
noolddir:
description:
- Keep rotated logs in the same directory as the original log.
type: bool
extension:
description:
- Extension to use for rotated log files (including dot).
- Useful when O(compress=false).
type: str
mail:
description:
- Mail logs to specified address when removed.
- Set to V(null) or omit to not mail logs.
type: str
mailfirst:
description:
- Mail just-created log file, not the about-to-expire one.
type: bool
maillast:
description:
- Mail about-to-expire log file (default).
type: bool
include:
description:
- Include additional configuration files from specified directory.
type: path
tabooext:
description:
- List of extensions that logrotate should not touch.
- Default is V(.rpmorig .rpmsave .v .swp .rpmnew .cfsaved .rhn-cfg-tmp-*).
- Set to V(null) or empty list to clear defaults.
type: list
elements: str
enabled:
description:
- Whether the configuration should be enabled.
- When V(false), adds V(.disabled) extension to the config file.
type: bool
start:
description:
- Base number for rotated files.
- For example, V(1) gives files .1, .2, and so on instead of .0, .1.
type: int
syslog:
description:
- Send logrotate messages to syslog.
type: bool
extends_documentation_fragment:
- community.general.attributes
"""
EXAMPLES = r"""
- name: Ensure logrotate config directory exists
ansible.builtin.file:
path: /etc/logrotate.d
state: directory
mode: '0755'
- name: Configure log rotation for Nginx
community.general.logrotate:
name: nginx
paths:
- /var/log/nginx/*.log
rotation_period: daily
rotate_count: 14
compress: true
compressoptions: "-9"
delaycompress: true
missingok: true
notifempty: true
create: "0640 www-data adm"
sharedscripts: true
postrotate:
- "[ -f /var/run/nginx.pid ] && kill -USR1 $(cat /var/run/nginx.pid)"
- "echo 'Nginx logs rotated'"
- name: Configure size-based rotation for application logs
community.general.logrotate:
name: myapp
paths:
- /var/log/myapp/app.log
- /var/log/myapp/debug.log
size: 100M
rotate_count: 10
compress: true
compressoptions: "-1"
dateext: true
dateyesterday: true
dateformat: -%Y%m%d.%s
missingok: true
copytruncate: true
- name: Configure log rotation with secure deletion
community.general.logrotate:
name: secure-app
paths:
- /var/log/secure-app/*.log
rotation_period: weekly
rotate_count: 4
shred: true
shredcycles: 3
compress: true
compressoptions: "-9"
- name: Configure log rotation with custom start number
community.general.logrotate:
name: custom-start
paths:
- /var/log/custom/*.log
rotation_period: monthly
rotate_count: 6
start: 1
compress: true
- name: Configure log rotation with old directory
community.general.logrotate:
name: with-olddir
paths:
- /opt/app/logs/*.log
rotation_period: weekly
rotate_count: 4
olddir: /var/log/archives
createolddir: true
compress: true
compression_method: zstd
- name: Disable logrotate configuration
community.general.logrotate:
name: old-service
enabled: false
- name: Remove logrotate configuration
community.general.logrotate:
name: deprecated-app
state: absent
- name: Complex configuration with multiple scripts
community.general.logrotate:
name: complex-app
paths:
- /var/log/complex/*.log
rotation_period: monthly
rotate_count: 6
compress: true
delaycompress: false
prerotate: |
echo "Starting rotation for complex app"
systemctl stop complex-app
postrotate: |
systemctl start complex-app
echo "Rotation completed"
logger -t logrotate "Complex app logs rotated"
firstaction: "echo 'First action: Starting batch rotation'"
lastaction: "echo 'Last action: Batch rotation complete'"
- name: User-specific logrotate configuration
community.general.logrotate:
name: myuser-apps
config_dir: ~/.logrotate.d
paths:
- ~/app/*.log
- ~/.cache/*/*.log
rotation_period: daily
rotate_count: 30
compress: true
su: "{{ ansible_user_id }} users"
- name: Configuration with copy instead of move
community.general.logrotate:
name: copy-config
paths:
- /var/log/copy-app/*.log
rotation_period: daily
rotate_count: 7
copy: true
- name: Configuration with syslog notifications
community.general.logrotate:
name: syslog-config
paths:
- /var/log/syslog-app/*.log
rotation_period: daily
rotate_count: 14
syslog: true
compress: true
- name: Configuration without compression
community.general.logrotate:
name: nocompress-config
paths:
- /var/log/nocompress/*.log
rotation_period: daily
rotate_count: 7
compress: false
- name: Configuration with custom taboo extensions
community.general.logrotate:
name: taboo-config
paths:
- /var/log/taboo/*.log
rotation_period: daily
rotate_count: 7
tabooext: [".backup", ".tmp", ".temp"]
"""
RETURN = r"""
config_file:
description: Path to the created/updated logrotate configuration file.
type: str
returned: when O(state=present)
sample: /etc/logrotate.d/nginx
config_content:
description: The generated logrotate configuration content.
type: str
returned: when O(state=present)
sample: |
/var/log/nginx/*.log {
daily
rotate 14
compress
compressoptions -9
delaycompress
missingok
notifempty
create 0640 www-data adm
sharedscripts
postrotate
[ -f /var/run/nginx.pid ] && kill -USR1 $(cat /var/run/nginx.pid)
echo 'Nginx logs rotated'
endscript
}
enabled_state:
description: Current enabled state of the configuration.
type: bool
returned: always
sample: true
"""
import os
import re
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.common.text.converters import to_native
class LogrotateConfig:
"""Logrotate configuration manager."""
def __init__(self, module: AnsibleModule, logrotate_bin: str) -> None:
self.module = module
self.params = module.params
self.logrotate_bin = logrotate_bin
self.result: dict[str, object] = {
"changed": False,
"config_file": "",
"config_content": "",
"enabled_state": True,
}
self.config_dir = self.params["config_dir"]
self.config_name = self.params["name"]
self.disabled_suffix = ".disabled"
self.config_file = self._get_config_path(self.params["enabled"])
def _get_config_path(self, enabled: bool) -> str:
"""Get config file path based on enabled state."""
base_path = os.path.join(self.config_dir, self.config_name)
if not enabled:
return base_path + self.disabled_suffix
return base_path
def _validate_parameters(self) -> None:
"""Validate module parameters."""
if self.params["state"] == "present":
existing_content = self._read_existing_config(any_state=True)
if not existing_content and not self.params.get("paths"):
self.module.fail_json(msg="'paths' parameter is required when creating a new configuration")
if self.params.get("size") and self.params.get("maxsize"):
self.module.fail_json(msg="'size' and 'maxsize' parameters are mutually exclusive")
if self.params.get("copy") and self.params.get("copytruncate"):
self.module.fail_json(msg="'copy' and 'copytruncate' are mutually exclusive")
if self.params.get("copy") and self.params.get("renamecopy"):
self.module.fail_json(msg="'copy' and 'renamecopy' are mutually exclusive")
if self.params.get("ifempty") and self.params.get("notifempty"):
self.module.fail_json(msg="'ifempty' and 'notifempty' are mutually exclusive")
if self.params.get("delaycompress") and self.params.get("nodelaycompress"):
self.module.fail_json(msg="'delaycompress' and 'nodelaycompress' are mutually exclusive")
if self.params.get("olddir") and self.params.get("noolddir"):
self.module.fail_json(msg="'olddir' and 'noolddir' are mutually exclusive")
if self.params.get("su"):
su_parts = self.params["su"].split()
if len(su_parts) != 2:
self.module.fail_json(msg="'su' parameter must be in format 'user group'")
if self.params.get("shredcycles", 1) < 1:
self.module.fail_json(msg="'shredcycles' must be a positive integer")
if self.params.get("start", 0) < 0:
self.module.fail_json(msg="'start' must be a non-negative integer")
for size_param in ["size", "minsize", "maxsize"]:
if self.params.get(size_param):
if not re.match(r"^\d+[kMG]?$", self.params[size_param], re.I):
self.module.fail_json(
msg=f"'{size_param}' must be in format 'number[k|M|G]' (for example '100M', '1G')"
)
def _read_existing_config(self, any_state: bool = False) -> str | None:
"""Read existing configuration file.
Args:
any_state: If True, check both enabled and disabled versions.
If False, only check based on current enabled param.
"""
if any_state:
for suffix in ["", self.disabled_suffix]:
config_path = os.path.join(self.config_dir, self.config_name + suffix)
if os.path.exists(config_path):
self.result["enabled_state"] = suffix == ""
try:
with open(config_path, "r") as f:
return f.read()
except Exception as e:
self.module.fail_json(msg=f"Failed to read config file {config_path}: {to_native(e)}")
else:
config_path = self._get_config_path(self.params["enabled"])
if os.path.exists(config_path):
try:
with open(config_path, "r") as f:
return f.read()
except Exception as e:
self.module.fail_json(msg=f"Failed to read config file {config_path}: {to_native(e)}")
return None
def _generate_config_content(self) -> str:
"""Generate logrotate configuration content."""
if not self.params.get("paths"):
existing_content = self._read_existing_config(any_state=True)
if existing_content:
lines = existing_content.strip().split("\n")
paths = []
for line in lines:
line = line.strip()
if (
line
and not line.startswith("#")
and not line.startswith("{")
and not line.startswith("}")
and "/" in line
):
if not any(
keyword in line
for keyword in [" ", "\t", "daily", "weekly", "monthly", "yearly", "rotate", "compress"]
):
paths.append(line)
if paths:
self.params["paths"] = paths
else:
self.params["paths"] = []
else:
self.module.fail_json(
msg="Cannot generate configuration: no paths specified and no existing configuration found"
)
lines = []
paths = self.params["paths"]
if isinstance(paths, str):
paths = [paths]
for path in paths:
lines.append(path)
lines.append("{")
lines.append("")
rotation_period = self.params.get("rotation_period")
if rotation_period is not None and not self.params.get("size") and not self.params.get("maxsize"):
lines.append(f" {rotation_period}")
if self.params.get("size") is not None:
lines.append(f" size {self.params['size']}")
elif self.params.get("maxsize") is not None:
lines.append(f" maxsize {self.params['maxsize']}")
if self.params.get("minsize") is not None:
lines.append(f" minsize {self.params['minsize']}")
rotate_count = self.params.get("rotate_count")
if rotate_count is not None:
lines.append(f" rotate {rotate_count}")
start_val = self.params.get("start")
if start_val is not None:
lines.append(f" start {start_val}")
compress_val = self.params.get("compress")
if compress_val is not None:
if compress_val:
comp_method = self.params.get("compression_method")
if comp_method is not None and comp_method != "gzip":
lines.append(f" compresscmd /usr/bin/{comp_method}")
if comp_method == "zstd":
lines.append(f" uncompresscmd /usr/bin/{comp_method} -d")
elif comp_method == "lz4":
lines.append(f" uncompresscmd /usr/bin/{comp_method} -d")
else:
lines.append(f" uncompresscmd /usr/bin/{comp_method}un{comp_method}")
lines.append(f" compressext .{comp_method}")
lines.append(" compress")
if self.params.get("compressoptions") is not None:
lines.append(f" compressoptions {self.params['compressoptions']}")
else:
lines.append(" nocompress")
delaycompress_val = self.params.get("delaycompress")
if delaycompress_val is not None:
if delaycompress_val:
lines.append(" delaycompress")
nodelaycompress_val = self.params.get("nodelaycompress")
if nodelaycompress_val is not None:
if nodelaycompress_val:
lines.append(" nodelaycompress")
shred_val = self.params.get("shred")
if shred_val is not None and shred_val:
lines.append(" shred")
shredcycles_val = self.params.get("shredcycles")
if shredcycles_val is not None:
lines.append(f" shredcycles {shredcycles_val}")
missingok_val = self.params.get("missingok")
if missingok_val is not None:
if not missingok_val:
lines.append(" nomissingok")
else:
lines.append(" missingok")
ifempty_val = self.params.get("ifempty")
notifempty_val = self.params.get("notifempty")
if ifempty_val is not None and ifempty_val:
lines.append(" ifempty")
elif notifempty_val is not None and notifempty_val:
lines.append(" notifempty")
create_val = self.params.get("create")
if create_val is not None:
lines.append(f" create {create_val}")
copy_val = self.params.get("copy")
renamecopy_val = self.params.get("renamecopy")
copytruncate_val = self.params.get("copytruncate")
if copy_val is not None and copy_val:
lines.append(" copy")
elif renamecopy_val is not None and renamecopy_val:
lines.append(" renamecopy")
elif copytruncate_val is not None and copytruncate_val:
lines.append(" copytruncate")
if self.params.get("maxage") is not None:
lines.append(f" maxage {self.params['maxage']}")
dateext_val = self.params.get("dateext")
if dateext_val is not None and dateext_val:
lines.append(" dateext")
dateyesterday_val = self.params.get("dateyesterday")
if dateyesterday_val is not None and dateyesterday_val:
lines.append(" dateyesterday")
dateformat_val = self.params.get("dateformat")
if dateformat_val is not None:
lines.append(f" dateformat {dateformat_val}")
sharedscripts_val = self.params.get("sharedscripts")
if sharedscripts_val is not None and sharedscripts_val:
lines.append(" sharedscripts")
if self.params.get("su") is not None:
lines.append(f" su {self.params['su']}")
noolddir_val = self.params.get("noolddir")
olddir_val = self.params.get("olddir")
if noolddir_val is not None and noolddir_val:
lines.append(" noolddir")
elif olddir_val is not None:
lines.append(f" olddir {olddir_val}")
createolddir_val = self.params.get("createolddir")
if createolddir_val is not None and createolddir_val:
lines.append(" createolddir")
if self.params.get("extension") is not None:
lines.append(f" extension {self.params['extension']}")
mail_val = self.params.get("mail")
if mail_val is not None:
lines.append(f" mail {mail_val}")
mailfirst_val = self.params.get("mailfirst")
maillast_val = self.params.get("maillast")
if mailfirst_val is not None and mailfirst_val:
lines.append(" mailfirst")
elif maillast_val is not None and maillast_val:
lines.append(" maillast")
if self.params.get("include") is not None:
lines.append(f" include {self.params['include']}")
if self.params.get("tabooext") is not None:
tabooext = self.params["tabooext"]
if isinstance(tabooext, list):
tabooext = " ".join(tabooext)
if tabooext.strip():
lines.append(f" tabooext {tabooext}")
syslog_val = self.params.get("syslog")
if syslog_val is not None and syslog_val:
lines.append(" syslog")
scripts = {
"prerotate": self.params.get("prerotate"),
"postrotate": self.params.get("postrotate"),
"firstaction": self.params.get("firstaction"),
"lastaction": self.params.get("lastaction"),
"preremove": self.params.get("preremove"),
}
for script_name, script_content in scripts.items():
if script_content is not None:
lines.append(f" {script_name}")
for command in script_content:
for line in command.strip().split("\n"):
if line.strip():
lines.append(f" {line}")
lines.append(" endscript")
lines.append("")
lines.append("}")
return "\n".join(lines)
def apply(self) -> dict[str, object]:
"""Apply logrotate configuration."""
self._validate_parameters()
state = self.params["state"]
if not os.path.exists(self.config_dir):
self.module.fail_json(
msg=f"Config directory '{self.config_dir}' does not exist. "
f"Please create it manually or ensure the correct path is specified."
)
if state == "absent":
for suffix in ["", self.disabled_suffix]:
config_path = os.path.join(self.config_dir, self.config_name + suffix)
if os.path.exists(config_path):
if not self.module.check_mode:
os.remove(config_path)
self.result["changed"] = True
self.result["config_file"] = config_path
break
return self.result
existing_content = self._read_existing_config(any_state=True)
current_enabled = self.result.get("enabled_state", True)
target_enabled = self.params.get("enabled")
if target_enabled is None:
target_enabled = current_enabled
only_changing_enabled = (
existing_content is not None and not self.params.get("paths") and target_enabled != current_enabled
)
if only_changing_enabled:
old_path = self._get_config_path(not target_enabled)
new_path = self._get_config_path(target_enabled)
if os.path.exists(old_path) and not os.path.exists(new_path):
self.result["changed"] = True
if not self.module.check_mode:
self.module.atomic_move(old_path, new_path, unsafe_writes=False)
self.result["config_file"] = new_path
self.result["enabled_state"] = target_enabled
try:
with open(new_path, "r") as f:
self.result["config_content"] = f.read()
except Exception:
self.result["config_content"] = existing_content
return self.result
new_content = self._generate_config_content()
self.result["config_content"] = new_content
self.result["config_file"] = self._get_config_path(target_enabled)
needs_update = False
if existing_content is None:
needs_update = True
elif existing_content != new_content:
needs_update = True
elif target_enabled != current_enabled:
needs_update = True
if needs_update:
self.result["changed"] = True
if not self.module.check_mode:
for suffix in ["", self.disabled_suffix]:
old_path = os.path.join(self.config_dir, self.config_name + suffix)
if os.path.exists(old_path):
self._backup_config(old_path)
os.remove(old_path)
try:
config_file_path = str(self.result["config_file"])
with open(config_file_path, "w") as f:
f.write(new_content)
os.chmod(config_file_path, 0o644)
except Exception as e:
self.module.fail_json(
msg=f"Failed to write config file {self.result['config_file']}: {to_native(e)}"
)
if not self.module.check_mode:
test_cmd = [self.logrotate_bin, "-d", str(self.result["config_file"])]
rc, stdout, stderr = self.module.run_command(test_cmd)
if rc != 0:
self.module.warn(f"logrotate configuration test failed: {stderr}")
self.result["enabled_state"] = target_enabled
return self.result
def main() -> None:
"""Main function."""
module = AnsibleModule(
argument_spec=dict(
name=dict(type="str", required=True, aliases=["config_name"]),
state=dict(type="str", choices=["present", "absent"]),
config_dir=dict(type="path"),
paths=dict(type="list", elements="path"),
rotation_period=dict(
type="str",
choices=["daily", "weekly", "monthly", "yearly"],
),
rotate_count=dict(type="int"),
compress=dict(type="bool"),
compressoptions=dict(type="str"),
compression_method=dict(
type="str",
choices=["gzip", "bzip2", "xz", "zstd", "lzma", "lz4"],
),
delaycompress=dict(type="bool"),
nodelaycompress=dict(type="bool"),
shred=dict(type="bool"),
shredcycles=dict(type="int"),
missingok=dict(type="bool"),
ifempty=dict(type="bool"),
notifempty=dict(type="bool"),
create=dict(type="str"),
copytruncate=dict(type="bool"),
copy=dict(type="bool"),
renamecopy=dict(type="bool"),
size=dict(type="str"),
minsize=dict(type="str"),
maxsize=dict(type="str"),
maxage=dict(type="int"),
dateext=dict(type="bool"),
dateyesterday=dict(type="bool"),
dateformat=dict(type="str"),
sharedscripts=dict(type="bool"),
prerotate=dict(type="list", elements="str"),
postrotate=dict(type="list", elements="str"),
firstaction=dict(type="list", elements="str"),
lastaction=dict(type="list", elements="str"),
preremove=dict(type="list", elements="str"),
su=dict(type="str"),
olddir=dict(type="path"),
createolddir=dict(type="bool"),
noolddir=dict(type="bool"),
extension=dict(type="str"),
mail=dict(type="str"),
mailfirst=dict(type="bool"),
maillast=dict(type="bool"),
include=dict(type="path"),
tabooext=dict(type="list", elements="str"),
enabled=dict(type="bool"),
start=dict(type="int"),
syslog=dict(type="bool"),
),
supports_check_mode=True,
)
logrotate_bin = module.get_bin_path("logrotate", required=True)
logrotate_config = LogrotateConfig(module, logrotate_bin)
result = logrotate_config.apply()
module.exit_json(**result)
if __name__ == "__main__":
main()