Код IT Загрузка примера кода…

Python main.py
#!/usr/bin/env python3
"""
Автоматическая архивация лог-файлов.

Скрипт просматривает указанный каталог, находит файлы старше заданного возраста,
сжимает их в tar.gz-архивы и перемещает в архивный каталог. Перед архивацией
проверяется свободное место. Все действия логируются в JSON-формате.

Требования:
- Python ≥ 3.8
- Не требует сторонних зависимостей.

Использование:
    python log_archiver.py \
        --source /var/log/app \
        --archive /var/log/archive \
        --days 30 \
        --min-free-space 1GB \
        --dry-run

Переменные окружения (альтернатива CLI):
    LOG_ARCHIVER_SOURCE      — каталог с логами
    LOG_ARCHIVER_ARCHIVE     — каталог архивов
    LOG_ARCHIVER_DAYS        — порог возраста (в днях)
    LOG_ARCHIVER_MIN_FREE    — минимальный объём свободного места (например, "2.5GB")
    LOG_ARCHIVER_DRY_RUN     — "1" для тестового прогона без изменений
    TELEGRAM_BOT_TOKEN       — токен бота (опционально, для алертов)
    TELEGRAM_CHAT_ID         — ID чата (опционально)
"""

import argparse
import json
import logging
import os
import shutil
import subprocess
import sys
import time

from datetime import datetime, timedelta
from pathlib import Path


# === Настройка логгера ===
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s %(levelname)s %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
    handlers=[
        logging.StreamHandler(sys.stdout),
        logging.FileHandler("/var/log/log_archiver.log", encoding="utf-8"),
    ],
)
logger = logging.getLogger(__name__)


def parse_size(size_str: str) -> int:
    """Преобразует строку вида '1.5GB' в байты."""
    size_str = size_str.upper().strip()
    multipliers = {"B": 1, "KB": 1024, "MB": 1024**2, "GB": 1024**3, "TB": 1024**4}
    for unit, mult in multipliers.items():
        if size_str.endswith(unit):
            try:
                value = float(size_str[: -len(unit)].strip())
                return int(value * mult)
            except ValueError:
                pass
    raise ValueError(f"Некорректный формат размера: {size_str}")


def get_free_space(path: Path) -> int:
    """Возвращает свободное место на устройстве в байтах."""
    stat = os.statvfs(path)
    return stat.f_bavail * stat.f_frsize


def archive_file(file_path: Path, archive_dir: Path, dry_run: bool = False) -> bool:
    """Архивирует один файл в tar.gz и удаляет оригинал при успехе."""
    try:
        archive_name = f"{file_path.stem}_{file_path.stat().st_mtime:.0f}.tar.gz"
        archive_path = archive_dir / archive_name

        if dry_run:
            logger.info(f"[DRY-RUN] Будет создан архив: {archive_path}")
            return True

        # Создаём архив с сохранением прав и времён
        cmd = [
            "tar",
            "-czf",
            str(archive_path),
            "-C",
            str(file_path.parent),
            file_path.name,
        ]
        result = subprocess.run(cmd, capture_output=True, text=True, check=True)
        logger.debug(f"tar stdout: {result.stdout}")
        logger.debug(f"tar stderr: {result.stderr}")

        # Удаляем оригинал только после успешного архивирования
        file_path.unlink()
        logger.info(f"Архивирован и удалён: {file_path}{archive_path}")
        return True

    except subprocess.CalledProcessError as e:
        logger.error(f"Ошибка tar для {file_path}: {e.stderr}")
        return False
    except Exception as e:
        logger.exception(f"Необработанная ошибка при архивации {file_path}: {e}")
        return False


def send_telegram_alert(message: str):
    """Отправляет уведомление в Telegram (если настроены токен и chat_id)."""
    bot_token = os.getenv("TELEGRAM_BOT_TOKEN")
    chat_id = os.getenv("TELEGRAM_CHAT_ID")
    if not bot_token or not chat_id:
        return

    try:
        import requests  # lazy import — только при необходимости
        url = f"https://api.telegram.org/bot{bot_token}/sendMessage"
        payload = {
            "chat_id": chat_id,
            "text": f"[LogArchiver] {message}",
            "parse_mode": "Markdown",
        }
        requests.post(url, json=payload, timeout=10)
    except Exception as e:
        logger.warning(f"Не удалось отправить Telegram-алерт: {e}")


def main():
    parser = argparse.ArgumentParser(description="Автоархивация логов")
    parser.add_argument("--source", required=False, help="Каталог с логами")
    parser.add_argument("--archive", required=False, help="Каталог для архивов")
    parser.add_argument("--days", type=int, default=30, help="Возраст файлов в днях")
    parser.add_argument(
        "--min-free-space", default="1GB", help="Минимум свободного места (напр., 2.5GB)"
    )
    parser.add_argument("--dry-run", action="store_true", help="Тестовый прогон")

    args = parser.parse_args()

    # Приоритет: CLI > ENV > default (но source/archive обязательны)
    source_dir = Path(args.source or os.getenv("LOG_ARCHIVER_SOURCE"))
    archive_dir = Path(args.archive or os.getenv("LOG_ARCHIVER_ARCHIVE"))
    days = args.days or int(os.getenv("LOG_ARCHIVER_DAYS", 30))
    min_free = args.min_free_space or os.getenv("LOG_ARCHIVER_MIN_FREE", "1GB")
    dry_run = args.dry_run or os.getenv("LOG_ARCHIVER_DRY_RUN") == "1"

    if not source_dir or not archive_dir:
        logger.error("Не указаны --source и --archive (или переменные окружения)")
        sys.exit(1)

    try:
        min_bytes = parse_size(min_free)
        cutoff_time = time.time() - days * 86400  # 86400 = секунд в дне
        archive_dir.mkdir(parents=True, exist_ok=True)

        # Проверка свободного места
        free_bytes = get_free_space(archive_dir)
        if free_bytes < min_bytes:
            msg = f"Недостаточно места в {archive_dir}: {free_bytes / 1e9:.2f} GB < {min_free}"
            logger.error(msg)
            send_telegram_alert(msg)
            sys.exit(1)

        # Сбор подходящих файлов
        candidates = [
            f
            for f in source_dir.rglob("*")
            if f.is_file() and f.stat().st_mtime < cutoff_time
        ]

        logger.info(f"Найдено {len(candidates)} файлов старше {days} дней в {source_dir}")

        success_count = 0
        for file in candidates:
            if archive_file(file, archive_dir, dry_run):
                success_count += 1

        summary = (
            f"Архивация завершена: {success_count}/{len(candidates)} файлов обработано."
        )
        logger.info(summary)
        if not dry_run and len(candidates) > 0:
            send_telegram_alert(summary)

    except Exception as e:
        error_msg = f"Критическая ошибка: {e}"
        logger.exception(error_msg)
        send_telegram_alert(error_msg)
        sys.exit(1)


if __name__ == "__main__":
    main()
#!/usr/bin/env python3
"""
Автоматическая архивация лог-файлов.

Скрипт просматривает указанный каталог, находит файлы старше заданного возраста,
сжимает их в tar.gz-архивы и перемещает в архивный каталог. Перед архивацией
проверяется свободное место. Все действия логируются в JSON-формате.

Требования:
- Python ≥ 3.8
- Не требует сторонних зависимостей.

Использование:
    python log_archiver.py \
        --source /var/log/app \
        --archive /var/log/archive \
        --days 30 \
        --min-free-space 1GB \
        --dry-run

Переменные окружения (альтернатива CLI):
    LOG_ARCHIVER_SOURCE      — каталог с логами
    LOG_ARCHIVER_ARCHIVE     — каталог архивов
    LOG_ARCHIVER_DAYS        — порог возраста (в днях)
    LOG_ARCHIVER_MIN_FREE    — минимальный объём свободного места (например, "2.5GB")
    LOG_ARCHIVER_DRY_RUN     — "1" для тестового прогона без изменений
    TELEGRAM_BOT_TOKEN       — токен бота (опционально, для алертов)
    TELEGRAM_CHAT_ID         — ID чата (опционально)
"""

import argparse
import json
import logging
import os
import shutil
import subprocess
import sys
import time

from datetime import datetime, timedelta
from pathlib import Path


# === Настройка логгера ===
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s %(levelname)s %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
    handlers=[
        logging.StreamHandler(sys.stdout),
        logging.FileHandler("/var/log/log_archiver.log", encoding="utf-8"),
    ],
)
logger = logging.getLogger(__name__)


def parse_size(size_str: str) -> int:
    """Преобразует строку вида '1.5GB' в байты."""
    size_str = size_str.upper().strip()
    multipliers = {"B": 1, "KB": 1024, "MB": 1024**2, "GB": 1024**3, "TB": 1024**4}
    for unit, mult in multipliers.items():
        if size_str.endswith(unit):
            try:
                value = float(size_str[: -len(unit)].strip())
                return int(value * mult)
            except ValueError:
                pass
    raise ValueError(f"Некорректный формат размера: {size_str}")


def get_free_space(path: Path) -> int:
    """Возвращает свободное место на устройстве в байтах."""
    stat = os.statvfs(path)
    return stat.f_bavail * stat.f_frsize


def archive_file(file_path: Path, archive_dir: Path, dry_run: bool = False) -> bool:
    """Архивирует один файл в tar.gz и удаляет оригинал при успехе."""
    try:
        archive_name = f"{file_path.stem}_{file_path.stat().st_mtime:.0f}.tar.gz"
        archive_path = archive_dir / archive_name

        if dry_run:
            logger.info(f"[DRY-RUN] Будет создан архив: {archive_path}")
            return True

        # Создаём архив с сохранением прав и времён
        cmd = [
            "tar",
            "-czf",
            str(archive_path),
            "-C",
            str(file_path.parent),
            file_path.name,
        ]
        result = subprocess.run(cmd, capture_output=True, text=True, check=True)
        logger.debug(f"tar stdout: {result.stdout}")
        logger.debug(f"tar stderr: {result.stderr}")

        # Удаляем оригинал только после успешного архивирования
        file_path.unlink()
        logger.info(f"Архивирован и удалён: {file_path}{archive_path}")
        return True

    except subprocess.CalledProcessError as e:
        logger.error(f"Ошибка tar для {file_path}: {e.stderr}")
        return False
    except Exception as e:
        logger.exception(f"Необработанная ошибка при архивации {file_path}: {e}")
        return False


def send_telegram_alert(message: str):
    """Отправляет уведомление в Telegram (если настроены токен и chat_id)."""
    bot_token = os.getenv("TELEGRAM_BOT_TOKEN")
    chat_id = os.getenv("TELEGRAM_CHAT_ID")
    if not bot_token or not chat_id:
        return

    try:
        import requests  # lazy import — только при необходимости
        url = f"https://api.telegram.org/bot{bot_token}/sendMessage"
        payload = {
            "chat_id": chat_id,
            "text": f"[LogArchiver] {message}",
            "parse_mode": "Markdown",
        }
        requests.post(url, json=payload, timeout=10)
    except Exception as e:
        logger.warning(f"Не удалось отправить Telegram-алерт: {e}")


def main():
    parser = argparse.ArgumentParser(description="Автоархивация логов")
    parser.add_argument("--source", required=False, help="Каталог с логами")
    parser.add_argument("--archive", required=False, help="Каталог для архивов")
    parser.add_argument("--days", type=int, default=30, help="Возраст файлов в днях")
    parser.add_argument(
        "--min-free-space", default="1GB", help="Минимум свободного места (напр., 2.5GB)"
    )
    parser.add_argument("--dry-run", action="store_true", help="Тестовый прогон")

    args = parser.parse_args()

    # Приоритет: CLI > ENV > default (но source/archive обязательны)
    source_dir = Path(args.source or os.getenv("LOG_ARCHIVER_SOURCE"))
    archive_dir = Path(args.archive or os.getenv("LOG_ARCHIVER_ARCHIVE"))
    days = args.days or int(os.getenv("LOG_ARCHIVER_DAYS", 30))
    min_free = args.min_free_space or os.getenv("LOG_ARCHIVER_MIN_FREE", "1GB")
    dry_run = args.dry_run or os.getenv("LOG_ARCHIVER_DRY_RUN") == "1"

    if not source_dir or not archive_dir:
        logger.error("Не указаны --source и --archive (или переменные окружения)")
        sys.exit(1)

    try:
        min_bytes = parse_size(min_free)
        cutoff_time = time.time() - days * 86400  # 86400 = секунд в дне
        archive_dir.mkdir(parents=True, exist_ok=True)

        # Проверка свободного места
        free_bytes = get_free_space(archive_dir)
        if free_bytes < min_bytes:
            msg = f"Недостаточно места в {archive_dir}: {free_bytes / 1e9:.2f} GB < {min_free}"
            logger.error(msg)
            send_telegram_alert(msg)
            sys.exit(1)

        # Сбор подходящих файлов
        candidates = [
            f
            for f in source_dir.rglob("*")
            if f.is_file() and f.stat().st_mtime < cutoff_time
        ]

        logger.info(f"Найдено {len(candidates)} файлов старше {days} дней в {source_dir}")

        success_count = 0
        for file in candidates:
            if archive_file(file, archive_dir, dry_run):
                success_count += 1

        summary = (
            f"Архивация завершена: {success_count}/{len(candidates)} файлов обработано."
        )
        logger.info(summary)
        if not dry_run and len(candidates) > 0:
            send_telegram_alert(summary)

    except Exception as e:
        error_msg = f"Критическая ошибка: {e}"
        logger.exception(error_msg)
        send_telegram_alert(error_msg)
        sys.exit(1)


if __name__ == "__main__":
    main()