mirror of
https://github.com/ansible-collections/community.general.git
synced 2026-02-04 07:51:50 +00:00
938 lines
32 KiB
Python
938 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
|
|
backup_file:
|
|
description: Path to the backup of the original configuration file, if it was backed up.
|
|
type: str
|
|
returned: when backup was made
|
|
sample: /etc/logrotate.d/nginx.20250101_120000
|
|
"""
|
|
|
|
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):
|
|
backup_path = self.module.backup_local(old_path)
|
|
if backup_path:
|
|
self.result["backup_file"] = backup_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()
|