/
psj
/
fstabeditor
Обзор
Документация
Войти
/
psj
/
fstabeditor
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
fstab_editor.py
962 строки
38 KB
qwen.ai[bot]
**Title:** Implement full localization and refactor fstab_editor.py
26 янв 2026, 11:15
26 янв 2026, 11:15
b2ab4a1
Код
Авторство
О чём код?
#!/usr/bin/env python3 """ fstab-editor - Утилита командной строки для безопасного управления записями /etc/fstab Этот файл является основной программой для управления записями fstab с полной локализацией на русский и английский языки, а также подробными комментариями на русском. """ import os import sys import subprocess import tempfile import shutil import syslog import argparse import datetime import stat import json from pathlib import Path import gettext import locale from typing import List, Dict, Optional, Tuple, Generator from functools import lru_cache import concurrent.futures # Глобальный переводчик _ = lambda s: s # Функция идентичности по умолчанию def setup_i18n(lang: Optional[str] = None) -> None: """ Настройка интернационализации Args: lang: Язык интерфейса (например, 'ru_RU' или 'en_US') """ global _ # Определение языка if lang is None: # Попытка получить язык из переменной окружения lang = os.environ.get('LANG', 'en_US') # Извлечение только части языка (до кодировки) lang = lang.split('.')[0].split('_')[0] # Сопоставление общих кодов языков с поддерживаемыми локалями if lang.startswith('ru'): lang = 'ru_RU' else: lang = 'en_US' # Настройка локали loc = locale.normalize(lang + '.UTF-8') try: locale.setlocale(locale.LC_ALL, loc) except locale.Error: # Если локаль недоступна, использовать значение по умолчанию pass # Настройка gettext try: # Определение пути к каталогу локализации localedir = os.path.join(os.path.dirname(__file__), 'locales') # Создание объекта перевода translation = gettext.translation( 'messages', localedir=localedir, languages=[lang], fallback=True ) # Установка перевода глобально _ = translation.gettext except OSError: # Если файлы перевода недоступны, использовать функцию идентичности по умолчанию _ = lambda s: s class FstabEditorError(Exception): """Базовое исключение для ошибок fstab-editor.""" def __init__(self, message: str, exit_code: int = 1): super().__init__(message) self.message = message self.exit_code = exit_code def log_message(message: str, level: int = syslog.LOG_INFO) -> None: """ Логирование сообщения через syslog Args: message: Сообщение для логирования level: Уровень важности сообщения """ try: syslog.syslog(level, f"fstab-editor: {message}") except: pass # Молча игнорировать ошибки, если syslog недоступен def print_error(message: str) -> None: """ Вывод сообщения об ошибке в stderr Args: message: Текст сообщения об ошибке """ print(f"[!] {message}", file=sys.stderr) def print_info(message: str) -> None: """ Вывод информационного сообщения в stderr Args: message: Текст информационного сообщения """ print(f"[i] {message}", file=sys.stderr) def print_success(message: str) -> None: """ Вывод сообщения об успешном завершении в stderr Args: message: Текст сообщения об успехе """ print(f"[OK] {message}", file=sys.stderr) def check_privileges() -> bool: """ Проверка наличия необходимых привилегий для изменения системных файлов Returns: True если есть привилегии root или доступны sudo/doas """ # Проверка, является ли пользователь root if os.getuid() == 0: return True # Проверка доступности sudo try: result = subprocess.run(['which', 'sudo'], capture_output=True, text=True, timeout=5) if result.returncode == 0: return True except: pass # Проверка доступности doas try: result = subprocess.run(['which', 'doas'], capture_output=True, text=True, timeout=5) if result.returncode == 0: return True except: pass return False def ensure_privileges() -> bool: """ Обеспечение привилегий для записи в /etc/fstab Raises: FstabEditorError: Если недоступны привилегии root, sudo или doas """ if os.getuid() == 0: return True # Попытка получения привилегий через sudo try: result = subprocess.run(['which', 'sudo'], capture_output=True, text=True, timeout=5) if result.returncode == 0: return True except: pass # Попытка получения привилегий через doas try: result = subprocess.run(['which', 'doas'], capture_output=True, text=True, timeout=5) if result.returncode == 0: return True except: pass print_error(_("Ошибка: необходимы права администратора. Установите sudo или doas.")) log_message("sudo/doas not available", syslog.LOG_ERR) raise FstabEditorError(_("Ошибка: необходимы права администратора. Установите sudo или doas."), 3) def find_fstab_path() -> str: """ Найти путь к файлу fstab Returns: Путь к файлу fstab """ return "/etc/fstab" def read_fstab() -> List[Dict[str, str]]: """ Чтение текущего файла fstab Returns: Список словарей, представляющих записи fstab """ fstab_path = find_fstab_path() if not os.path.exists(fstab_path): raise FstabEditorError(_("Файл {} не найден.").format(fstab_path), 4) with open(fstab_path, 'r', encoding='utf-8') as f: lines = f.readlines() entries = [] for i, line in enumerate(lines): line = line.strip() # Пропуск пустых строк и комментариев if not line or line.startswith('#'): continue parts = line.split() if len(parts) >= 6: entry = { 'device': parts[0], 'mountpoint': parts[1], 'type': parts[2], 'options': parts[3], 'dump': parts[4], 'pass': parts[5], '_line_number': i, '_original_line': line } entries.append(entry) return entries def write_fstab(entries: List[Dict[str, str]], backup: bool = True) -> None: """ Запись записей обратно в fstab с атомарной операцией Args: entries: Список записей fstab для записи backup: Создать резервную копию перед записью """ ensure_privileges() fstab_path = find_fstab_path() original_exists = os.path.exists(fstab_path) if backup and original_exists: # Создание резервной копии timestamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S") backup_path = f"{fstab_path}.bak.{timestamp}" if os.getuid() != 0: # Использовать sudo для копирования файла subprocess.run(['sudo', 'cp', fstab_path, backup_path], check=True, timeout=10) else: shutil.copy2(fstab_path, backup_path) print_success(f"Создана резервная копия: {backup_path}") log_message(f"backup created: {backup_path}") # Подготовка нового содержимого lines = [] for entry in entries: line = f"{entry['device']} {entry['mountpoint']} {entry['type']} {entry['options']} {entry['dump']} {entry['pass']}" lines.append(line) # Добавление комментария для указания, что файл был сгенерирован нашей утилитой header_lines = [ "# Generated by fstab-editor", "# DO NOT EDIT MANUALLY unless you know what you're doing", "" ] all_lines = header_lines + lines + [""] # Добавление конечного символа новой строки # Запись во временный файл сначала temp_fd, temp_path = tempfile.mkstemp(dir="/tmp", prefix="fstab_", suffix=".tmp") try: with os.fdopen(temp_fd, 'w', encoding='utf-8') as f: f.write('\n'.join(all_lines)) # Атомарное перемещение временного файла в целевой if os.getuid() != 0: # Использовать sudo для перемещения файла subprocess.run(['sudo', 'mv', temp_path, fstab_path], check=True, timeout=10) else: os.rename(temp_path, fstab_path) # Установка правильных разрешений if os.getuid() != 0: subprocess.run(['sudo', 'chmod', '644', fstab_path], check=True, timeout=10) else: os.chmod(fstab_path, 0o644) except Exception as e: # Очистка временного файла при ошибке try: os.unlink(temp_path) except: pass raise e def confirm_action(prompt: str) -> bool: """ Запрос подтверждения у пользователя перед продолжением Args: prompt: Текст запроса подтверждения Returns: True если пользователь подтвердил действие """ response = input(f"{prompt} (y/N): ").strip().lower() return response in ['y', 'yes'] def validate_mountpoint(mountpoint: str) -> None: """ Проверка существования точки монтирования Args: mountpoint: Путь к точке монтирования """ if not os.path.exists(mountpoint): raise FstabEditorError(_("Каталог {} не существует.").format(mountpoint), 4) @lru_cache(maxsize=128) def _get_supported_filesystems() -> set: """ Кэшированная функция для получения списка поддерживаемых файловых систем Returns: Множество названий поддерживаемых файловых систем """ try: with open('/proc/filesystems', 'r') as f: content = f.read() # Извлекаем и нормализуем названия файловых систем supported_fs = set() for line in content.splitlines(): # Извлекаем последнюю часть строки после табуляции fs_name = line.strip().split('\t')[-1].lower() supported_fs.add(fs_name) return supported_fs except FileNotFoundError: # Если /proc/filesystems недоступен, возвращаем пустое множество return set() def validate_device(device: str) -> None: """ Проверка существования устройства с помощью blkid Args: device: Путь к устройству (UUID=xxx, LABEL=xxx или прямой путь) """ try: # Обработка форматов UUID и LABEL if device.startswith('UUID='): uuid = device[5:] result = subprocess.run(['blkid', '-U', uuid], capture_output=True, text=True, timeout=10) if result.returncode != 0 or not result.stdout.strip(): raise FstabEditorError(_("Устройство не найдено: {}.").format(device), 2) elif device.startswith('LABEL='): label = device[6:] result = subprocess.run(['blkid', '-L', label], capture_output=True, text=True, timeout=10) if result.returncode != 0 or not result.stdout.strip(): raise FstabEditorError(_("Устройство не найдено: {}.").format(device), 2) else: # Прямой путь к устройству if not os.path.exists(device): raise FstabEditorError(_("Устройство не найдено: {}.").format(device), 2) except subprocess.TimeoutExpired: raise FstabEditorError(_("Таймаут при проверке устройства: {}.").format(device), 2) except subprocess.CalledProcessError: raise FstabEditorError(_("Устройство не найдено: {}.").format(device), 2) def validate_filesystem_type(fs_type: str) -> None: """ Проверка, что тип файловой системы поддерживается ядром Args: fs_type: Тип файловой системы """ # Получение списка поддерживаемых файловых систем из кэшированной функции supported_fs = _get_supported_filesystems() # Проверка, находится ли fs_type в списке (без учета регистра) if supported_fs and fs_type.lower() not in supported_fs: raise FstabEditorError(_("Неподдерживаемый тип файловой системы: {}.").format(fs_type), 2) # Если /proc/filesystems недоступен, пропустить эту проверку def validate_options_for_fs(options: str, fs_type: str) -> None: """ Проверка параметров монтирования на основе типа файловой системы Args: options: Параметры монтирования fs_type: Тип файловой системы """ # Базовая проверка - это нужно расширить для полной проверки option_list = options.split(',') # Пример проверок для конкретных типов файловых систем if fs_type in ['btrfs', 'xfs']: for opt in option_list: if opt.startswith('compress='): # Эти файловые системы поддерживают сжатие continue elif fs_type in ['ext4', 'ext3', 'ext2']: for opt in option_list: if opt.startswith('compress='): # Файловые системы ext* не поддерживают сжатие через параметры монтирования raise FstabEditorError(_("Опция '{}' недопустима для {}.").format(opt, fs_type), 2) # Проверка специальных параметров for opt in option_list: if '=' in opt: key, value = opt.split('=', 1) if key in ['uid', 'gid']: try: int(value) except ValueError: raise FstabEditorError(_("Значение '{}' для опции '{}' должно быть числом.").format(value, key), 2) def validate_dump_pass(dump: str, pass_value: str) -> None: """ Проверка значений dump и pass Args: dump: Значение dump pass_value: Значение pass """ try: dump_int = int(dump) pass_int = int(pass_value) if dump_int < 0 or pass_int < 0: raise FstabEditorError(_("Значения dump и pass должны быть целыми числами ≥ 0."), 1) except ValueError: raise FstabEditorError(_("Значения dump и pass должны быть целыми числами ≥ 0."), 1) def validate_entry(entry: Dict[str, str]) -> None: """ Проверка одной записи fstab Args: entry: Словарь, представляющий одну запись fstab """ validate_mountpoint(entry['mountpoint']) validate_device(entry['device']) validate_filesystem_type(entry['type']) validate_options_for_fs(entry['options'], entry['type']) validate_dump_pass(entry['dump'], entry['pass']) def validate_all_entries() -> bool: """ Проверка всех записей в fstab Returns: True если все записи действительны """ entries = read_fstab() errors = [] # Используем ThreadPoolExecutor для параллельной валидации записей with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor: # Создаем список задач валидации future_to_entry = {executor.submit(validate_entry, entry): entry for entry in entries} # Собираем результаты for future in concurrent.futures.as_completed(future_to_entry): entry = future_to_entry[future] try: future.result() # Вызовет исключение, если валидация не прошла except FstabEditorError as e: errors.append(f"{entry['mountpoint']}: {e.message}") if errors: for error in errors: print_error(error) return False return True def list_entries(show_comments: bool = False) -> None: """ Список всех записей в fstab Args: show_comments: Показывать комментарии в выводе """ # Читаем все строки файла, а не только действительные записи fstab_path = find_fstab_path() if not os.path.exists(fstab_path): raise FstabEditorError(_("Файл {} не найден.").format(fstab_path), 4) with open(fstab_path, 'r', encoding='utf-8') as f: lines = f.readlines() if show_comments: # Вывести все строки файла for line in lines: print(line.rstrip()) return # Только действительные записи entries = read_fstab() if not entries: print_info("Нет записей в /etc/fstab") return # Вывести заголовок print(f"{'DEVICE':<20} {'MOUNTPOINT':<15} {'TYPE':<8} {'OPTIONS':<15} {'DUMP':<5} {'PASS':<5}") for entry in entries: print(f"{entry['device']:<20} {entry['mountpoint']:<15} {entry['type']:<8} {entry['options']:<15} {entry['dump']:<5} {entry['pass']:<5}") def add_entry(device: str, mountpoint: str, fs_type: str, options: str, dump: str, pass_val: str) -> None: """ Добавить новую запись в fstab Args: device: Путь к устройству mountpoint: Точка монтирования fs_type: Тип файловой системы options: Параметры монтирования dump: Значение dump pass_val: Значение pass """ # Проверка входных данных validate_mountpoint(mountpoint) validate_device(device) validate_filesystem_type(fs_type) validate_options_for_fs(options, fs_type) validate_dump_pass(dump, pass_val) # Создание новой записи new_entry = { 'device': device, 'mountpoint': mountpoint, 'type': fs_type, 'options': options, 'dump': dump, 'pass': pass_val } # Показать, что будет добавлено, и запросить подтверждение entry_str = f"{device} {mountpoint} {fs_type} {options} {dump} {pass_val}" print(f"Добавить следующую запись?\n {entry_str}") if not confirm_action("Подтвердите"): print_error("Добавление отменено.") log_message(f"add cancelled for MOUNTPOINT={mountpoint}", syslog.LOG_WARNING) raise FstabEditorError("Действие отменено пользователем", 5) # Чтение существующих записей и добавление новой entries = read_fstab() entries.append(new_entry) # Запись обратно в fstab write_fstab(entries) print_success(f"Запись добавлена: {mountpoint}") log_message(f"entry added: MOUNTPOINT={mountpoint} DEVICE={device} TYPE={fs_type}") def edit_entry(mountpoint: str, device: Optional[str] = None, fs_type: Optional[str] = None, options: Optional[str] = None, dump: Optional[str] = None, pass_val: Optional[str] = None) -> None: """ Редактировать существующую запись в fstab Args: mountpoint: Точка монтирования device: Путь к устройству (необязательно) fs_type: Тип файловой системы (необязательно) options: Параметры монтирования (необязательно) dump: Значение dump (необязательно) pass_val: Значение pass (необязательно) """ entries = read_fstab() # Найти запись для редактирования target_entry = None for entry in entries: if entry['mountpoint'] == mountpoint: target_entry = entry break if not target_entry: raise FstabEditorError(f'Запись с точкой монтирования "{mountpoint}" не найдена.', 1) # Сохранить исходные значения для сравнения original_values = dict(target_entry) # Обновить поля, если они предоставлены if device is not None: target_entry['device'] = device if fs_type is not None: target_entry['type'] = fs_type if options is not None: target_entry['options'] = options if dump is not None: target_entry['dump'] = dump if pass_val is not None: target_entry['pass'] = pass_val # Проверить, были ли изменения changes_made = any( str(original_values[key]) != str(target_entry[key]) for key in ['device', 'type', 'options', 'dump', 'pass'] ) if not changes_made: print_info(f"Нет изменений для записи с точкой монтирования {mountpoint}.") log_message(f"no changes for MOUNTPOINT={mountpoint}", syslog.LOG_WARNING) return # Проверить обновленную запись validate_entry(target_entry) # Показать изменения и запросить подтверждение old_str = f"{original_values['device']} {original_values['mountpoint']} {original_values['type']} {original_values['options']} {original_values['dump']} {original_values['pass']}" new_str = f"{target_entry['device']} {target_entry['mountpoint']} {target_entry['type']} {target_entry['options']} {target_entry['dump']} {target_entry['pass']}" print(f"Изменения для {mountpoint}\nБыло: {old_str}\nСтало: {new_str}") if not confirm_action("Подтвердить сохранение"): print_error("Редактирование отменено.") log_message(f"edit cancelled for MOUNTPOINT={mountpoint}", syslog.LOG_WARNING) raise FstabEditorError("Действие отменено пользователем", 5) # Запись обратно в fstab write_fstab(entries) print_success(f"Запись для {mountpoint} обновлена.") log_message(f"entry updated: MOUNTPOINT={mountpoint} CHANGED=" + ",".join([k for k in ['device', 'type', 'options', 'dump', 'pass'] if str(original_values[k]) != str(target_entry[k])])) def delete_entry(mountpoint: str) -> None: """ Удалить запись из fstab Args: mountpoint: Точка монтирования для удаления """ entries = read_fstab() # Найти и удалить запись entry_to_delete = None for i, entry in enumerate(entries): if entry['mountpoint'] == mountpoint: entry_to_delete = entry break if not entry_to_delete: raise FstabEditorError(f'Запись с точкой монтирования "{mountpoint}" не найдена.', 1) # Показать, что будет удалено, и запросить подтверждение entry_str = f"{entry_to_delete['device']} {entry_to_delete['mountpoint']} {entry_to_delete['type']} {entry_to_delete['options']} {entry_to_delete['dump']} {entry_to_delete['pass']}" print(f"Удалить следующую запись?\n {entry_str}") if not confirm_action("Подтвердите"): print_error("Удаление отменено.") log_message(f"deletion cancelled for MOUNTPOINT={mountpoint}", syslog.LOG_WARNING) raise FstabEditorError("Действие отменено пользователем", 5) entries.remove(entry_to_delete) # Запись обратно в fstab write_fstab(entries) print_success(f"Запись для {mountpoint} удалена.") log_message(f"entry deleted: MOUNTPOINT={mountpoint} DEVICE={entry_to_delete['device']}") def create_backup(filename: Optional[str] = None) -> str: """ Создать резервную копию fstab Args: filename: Путь к файлу резервной копии (по умолчанию генерируется автоматически) Returns: Путь к созданному файлу резервной копии """ fstab_path = find_fstab_path() if not os.path.exists(fstab_path): raise FstabEditorError(f"/etc/fstab не найден.", 4) if filename is None: timestamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S") filename = f"{fstab_path}.bak.{timestamp}" ensure_privileges() if os.getuid() != 0: subprocess.run(['sudo', 'cp', fstab_path, filename], check=True, timeout=10) else: shutil.copy2(fstab_path, filename) print_success(f"Создана резервная копия: {filename}") log_message(f"backup created: {filename}") return filename def list_backups() -> None: """ Список всех резервных копий fstab """ fstab_path = find_fstab_path() backup_pattern = f"{fstab_path}.bak." backups = [] fstab_dir = os.path.dirname(fstab_path) for filename in os.listdir(fstab_dir): if filename.startswith(os.path.basename(fstab_path) + ".bak."): full_path = os.path.join(fstab_dir, filename) if os.path.isfile(full_path): mtime = os.path.getmtime(full_path) dt = datetime.datetime.fromtimestamp(mtime) formatted_date = dt.strftime("%d %b %Y, %H:%M") backups.append((full_path, formatted_date)) if not backups: print_error("Резервные копии не найдены.") return # Сортировка по времени модификации (новые первыми) backups.sort(key=lambda x: os.path.getmtime(x[0]), reverse=True) for backup_path, formatted_date in backups: print(f"{backup_path} ({formatted_date})") def restore_from_backup(filename: Optional[str] = None) -> None: """ Восстановить fstab из резервной копии Args: filename: Путь к файлу резервной копии (по умолчанию используется последняя) """ fstab_path = find_fstab_path() if filename is None: # Найти самую последнюю резервную копию backup_files = [] fstab_dir = os.path.dirname(fstab_path) for f in os.listdir(fstab_dir): if f.startswith(os.path.basename(fstab_path) + ".bak."): full_path = os.path.join(fstab_dir, f) if os.path.isfile(full_path): backup_files.append(full_path) if not backup_files: raise FstabEditorError("Не найдено резервных копий.", 4) # Сортировка по времени модификации (новые первими) backup_files.sort(key=lambda x: os.path.getmtime(x), reverse=True) filename = backup_files[0] if not os.path.exists(filename): raise FstabEditorError(f"Файл не найден: {filename}", 4) # Прочитать содержимое резервной копии и проверить его try: with open(filename, 'r', encoding='utf-8') as f: backup_content = f.read() # Простая проверка - попытка разбора записей temp_entries = [] for line in backup_content.splitlines(): line = line.strip() if not line or line.startswith('#'): continue parts = line.split() if len(parts) >= 6: entry = { 'device': parts[0], 'mountpoint': parts[1], 'type': parts[2], 'options': parts[3], 'dump': parts[4], 'pass': parts[5] } temp_entries.append(entry) # Проверка каждой записи # Используем многопоточность для ускорения валидации with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor: futures = [executor.submit(validate_entry, entry) for entry in temp_entries] for future in futures: future.result() # Вызовет исключение, если валидация не прошла except Exception as e: raise FstabEditorError("Резервная копия содержит ошибки.", 2) # Запрос подтверждения print(f"Восстановить {fstab_path} из {filename}?") if not confirm_action("Подтвердите"): print_error("Восстановление отменено.") log_message("restore cancelled", syslog.LOG_WARNING) raise FstabEditorError("Действие отменено пользователем", 5) ensure_privileges() if os.getuid() != 0: subprocess.run(['sudo', 'cp', filename, fstab_path], check=True, timeout=10) else: shutil.copy2(filename, fstab_path) print_success(f"{fstab_path} восстановлен.") log_message(f"fstab restored from: {filename}") def main(): """Основная функция программы""" # Настройка интернационализации setup_i18n() parser = argparse.ArgumentParser(description=_("fstab-editor - редактор файловой системы монтирования")) parser.add_argument('--lang', choices=['ru', 'en'], help=_("Язык интерфейса (ru или en)")) parser.add_argument('--no-color', action='store_true', help=_("Отключить цветной вывод")) parser.add_argument('--verbose', action='store_true', help=_("Подробный вывод")) subparsers = parser.add_subparsers(dest='command', help=_('Доступные команды')) # Команда списка list_parser = subparsers.add_parser('list', help=_('Список записей в /etc/fstab')) list_parser.add_argument('--show-comments', action='store_true', help=_('Показать комментарии в выводе')) # Команда добавления add_parser = subparsers.add_parser('add', help=_('Добавить новую запись в /etc/fstab')) add_parser.add_argument('device', help=_('Путь к устройству (UUID=xxx, LABEL=xxx или путь)')) add_parser.add_argument('mountpoint', help=_('Точка монтирования')) add_parser.add_argument('type', help=_('Тип файловой системы (ext4, xfs, btrfs и т.д.)')) add_parser.add_argument('options', help=_('Опции монтирования (defaults, noatime и т.д.)')) add_parser.add_argument('dump', help=_('Значение dump (обычно 0)')) add_parser.add_argument('pass_val', help=_('Значение pass (обычно 0 или 1)')) # Команда редактирования edit_parser = subparsers.add_parser('edit', help=_('Редактировать существующую запись')) edit_parser.add_argument('mountpoint', help=_('Точка монтирования')) edit_parser.add_argument('--device', help=_('--device')) edit_parser.add_argument('--type', help=_('--type')) edit_parser.add_argument('--options', help=_('--options')) edit_parser.add_argument('--dump', help=_('--dump')) edit_parser.add_argument('--pass', help=_('--pass'), dest='pass_val') # Команда удаления del_parser = subparsers.add_parser('del', help=_('Удалить запись по точке монтирования')) del_parser.add_argument('mountpoint', help=_('Точка монтирования')) # Команда проверки subparsers.add_parser('validate', help=_('Проверить все записи в /etc/fstab')) # Команда резервного копирования backup_parser = subparsers.add_parser('backup', help=_('Создать резервную копию /etc/fstab')) backup_parser.add_argument('filename', nargs='?', help=_('Путь к файлу резервной копии')) # Команда списка резервных копий subparsers.add_parser('list-backups', help=_('Список всех резервных копий')) # Команда восстановления restore_parser = subparsers.add_parser('restore', help=_('Восстановить /etc/fstab из резервной копии')) restore_parser.add_argument('filename', nargs='?', help=_('Путь к файлу восстановления')) # Команда TUI subparsers.add_parser('tui', help=_('Запустить текстовый пользовательский интерфейс')) args = parser.parse_args() # Инициализация логирования try: syslog.openlog("fstab-editor", syslog.LOG_PID, syslog.LOG_USER) except: pass # Продолжить работу даже если инициализация syslog завершится неудачно try: if args.lang: setup_i18n(args.lang) if args.command == 'list': list_entries(args.show_comments) elif args.command == 'add': add_entry(args.device, args.mountpoint, args.type, args.options, args.dump, args.pass_val) elif args.command == 'edit': edit_entry(args.mountpoint, args.device, args.type, args.options, args.dump, args.pass_val) elif args.command == 'del': delete_entry(args.mountpoint) elif args.command == 'validate': if validate_all_entries(): print_success("Все записи в /etc/fstab корректны.") else: sys.exit(2) elif args.command == 'backup': create_backup(args.filename) elif args.command == 'list-backups': list_backups() elif args.command == 'restore': restore_from_backup(args.filename) elif args.command == 'tui': if not os.isatty(sys.stdin.fileno()): print_error("TUI requires a terminal. Use CLI commands instead.") log_message("TUI launched in non-TTY environment", syslog.LOG_ERR) sys.exit(1) print_error("TUI интерфейс временно недоступен. Используйте CLI команды.") log_message("TUI session started", syslog.LOG_INFO) # В реальной реализации здесь запускался бы TUI elif args.command is None: parser.print_help() else: print_error(f"Неизвестная команда: {args.command}") parser.print_help() sys.exit(1) except FstabEditorError as e: print_error(e.message) sys.exit(e.exit_code) except KeyboardInterrupt: print_error("\nОперация отменена пользователем.") sys.exit(5) except Exception as e: print_error(f"Внутренняя ошибка: {str(e)}") log_message(f"internal error: {str(e)}", syslog.LOG_ERR) sys.exit(4) finally: try: syslog.closelog() except: pass if __name__ == "__main__": main()