mirror of
https://github.com/andrey271192/safe-monthly-maintenance.git
synced 2026-09-20 11:55:33 +00:00
Add safe monthly maintenance installer
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
*.log
|
||||||
|
*.tmp
|
||||||
|
.DS_Store
|
||||||
12
CHANGELOG.md
Normal file
12
CHANGELOG.md
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## 1.0.2 — 2026-07-31
|
||||||
|
|
||||||
|
- Первый публичный релиз.
|
||||||
|
- Ежемесячный systemd timer с случайной задержкой.
|
||||||
|
- Безопасное APT-обновление без удаления пакетов.
|
||||||
|
- Проверки `dpkg`, свободного места и APT-lock.
|
||||||
|
- Очистка APT cache, systemd journal и временных файлов.
|
||||||
|
- Автоматическое определение нового ядра без автоматического reboot.
|
||||||
|
- `autoremove` и Docker prune выключены по умолчанию.
|
||||||
|
- Подробная документация и troubleshooting.
|
||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Andrey (andrey271192)
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
298
README.md
Normal file
298
README.md
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
# Safe Monthly Maintenance
|
||||||
|
|
||||||
|
Безопасное ежемесячное обслуживание Ubuntu/Debian одной командой.
|
||||||
|
|
||||||
|
Скрипт устанавливает systemd timer, обновляет пакеты без `dist-upgrade`, чистит
|
||||||
|
безопасные кэши и журналы, проверяет состояние `dpkg`, свободное место и
|
||||||
|
необходимость перезагрузки. Автоматическая перезагрузка, `autoremove` и очистка
|
||||||
|
Docker выключены по умолчанию.
|
||||||
|
|
||||||
|
> Проверено на Debian 12. Скрипт рассчитан на Ubuntu и Debian с systemd и APT.
|
||||||
|
> Перед массовым развертыванием проверьте его на одной тестовой VM.
|
||||||
|
|
||||||
|
## Установка одной командой
|
||||||
|
|
||||||
|
Закрепленная версия:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/andrey271192/safe-monthly-maintenance/v1.0.2/install.sh | sudo bash -s -- --run-now
|
||||||
|
```
|
||||||
|
|
||||||
|
Команда:
|
||||||
|
|
||||||
|
1. скачивает `install.sh`;
|
||||||
|
2. устанавливает maintenance-скрипт и systemd units;
|
||||||
|
3. включает ежемесячный timer;
|
||||||
|
4. сразу выполняет первое обслуживание.
|
||||||
|
|
||||||
|
Установка без немедленного запуска:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/andrey271192/safe-monthly-maintenance/v1.0.2/install.sh | sudo bash
|
||||||
|
```
|
||||||
|
|
||||||
|
Если `curl` отсутствует:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wget -qO- https://raw.githubusercontent.com/andrey271192/safe-monthly-maintenance/v1.0.2/install.sh | sudo bash -s -- --run-now
|
||||||
|
```
|
||||||
|
|
||||||
|
## Более безопасная установка с проверкой
|
||||||
|
|
||||||
|
Не запускайте неизвестный код от `root` вслепую. Скачайте, проверьте, затем
|
||||||
|
выполните:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/andrey271192/safe-monthly-maintenance/v1.0.2/install.sh -o /tmp/install-safe-maintenance.sh
|
||||||
|
sha256sum /tmp/install-safe-maintenance.sh
|
||||||
|
less /tmp/install-safe-maintenance.sh
|
||||||
|
sudo bash /tmp/install-safe-maintenance.sh --run-now
|
||||||
|
```
|
||||||
|
|
||||||
|
SHA-256 версии `v1.0.2`:
|
||||||
|
|
||||||
|
```text
|
||||||
|
247312aaca5eb5b40c8893edb065203b2e4f0121c47421d85b46f9ae46770367
|
||||||
|
```
|
||||||
|
|
||||||
|
## Что устанавливается
|
||||||
|
|
||||||
|
| Путь | Назначение |
|
||||||
|
|---|---|
|
||||||
|
| `/usr/local/sbin/safe-monthly-maintenance` | Основной maintenance-скрипт |
|
||||||
|
| `/etc/default/safe-monthly-maintenance` | Настройки конкретной VM |
|
||||||
|
| `/etc/systemd/system/safe-monthly-maintenance.service` | Одноразовый systemd service |
|
||||||
|
| `/etc/systemd/system/safe-monthly-maintenance.timer` | Ежемесячный запуск |
|
||||||
|
| `/var/backups/safe-monthly-maintenance/` | Снимки списка пакетов |
|
||||||
|
|
||||||
|
Повторный запуск installer обновляет скрипт и units, но сохраняет существующий
|
||||||
|
файл настроек.
|
||||||
|
|
||||||
|
## Расписание
|
||||||
|
|
||||||
|
Timer запускается первого числа каждого месяца в 03:00 с случайной задержкой до
|
||||||
|
шести часов:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
OnCalendar=*-*-01 03:00:00
|
||||||
|
RandomizedDelaySec=6h
|
||||||
|
Persistent=true
|
||||||
|
```
|
||||||
|
|
||||||
|
Случайная задержка уменьшает одновременную нагрузку на репозитории, если один
|
||||||
|
installer установлен на много VM. `Persistent=true` выполняет пропущенный запуск
|
||||||
|
после включения сервера.
|
||||||
|
|
||||||
|
## Что делает обслуживание
|
||||||
|
|
||||||
|
### 1. Проверяет окружение
|
||||||
|
|
||||||
|
- требует права `root`;
|
||||||
|
- разрешает только Ubuntu/Debian и Debian-подобные системы;
|
||||||
|
- использует `flock`, поэтому два запуска не идут одновременно;
|
||||||
|
- использует ожидание APT-lock до 600 секунд;
|
||||||
|
- повторяет сетевые обращения APT до трех раз.
|
||||||
|
|
||||||
|
### 2. Сохраняет список пакетов
|
||||||
|
|
||||||
|
Перед обновлением выполняется:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dpkg --get-selections
|
||||||
|
```
|
||||||
|
|
||||||
|
Снимок записывается в `/var/backups/safe-monthly-maintenance/`. Снимки старше
|
||||||
|
120 дней удаляются. Это не полный rollback системы, но помогает восстановить
|
||||||
|
состав пакетов.
|
||||||
|
|
||||||
|
### 3. Чистит только безопасные данные
|
||||||
|
|
||||||
|
- `systemd-tmpfiles --clean` — применяет штатные политики очистки временных
|
||||||
|
файлов;
|
||||||
|
- `logrotate` — запускает штатную ротацию логов;
|
||||||
|
- `journalctl --vacuum-time=30d` — удаляет старый journal;
|
||||||
|
- `journalctl --vacuum-size=500M` — ограничивает размер journal;
|
||||||
|
- `apt-get autoclean` — удаляет устаревшие package-файлы;
|
||||||
|
- `apt-get clean` — очищает скачанный APT-кэш после обновления.
|
||||||
|
|
||||||
|
Скрипт не выполняет ручной `rm -rf` для `/tmp`, `/var/log`, Docker volumes или
|
||||||
|
пользовательских каталогов.
|
||||||
|
|
||||||
|
### 4. Проверяет пакетную систему
|
||||||
|
|
||||||
|
`dpkg --audit` запускается до обновления. Если пакетная операция была прервана,
|
||||||
|
обновление пропускается. Скрипт не пытается автоматически ремонтировать
|
||||||
|
неоднозначное состояние.
|
||||||
|
|
||||||
|
После обновления выполняется:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
apt-get check
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Проверяет свободное место
|
||||||
|
|
||||||
|
Обновление пропускается, если:
|
||||||
|
|
||||||
|
- на `/` осталось меньше 1 ГиБ;
|
||||||
|
- на отдельном `/boot` осталось меньше 200 МиБ.
|
||||||
|
|
||||||
|
Порог можно изменить в конфигурации.
|
||||||
|
|
||||||
|
### 6. Обновляет пакеты
|
||||||
|
|
||||||
|
Используется:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
apt-get -y \
|
||||||
|
-o Dpkg::Options::=--force-confold \
|
||||||
|
--with-new-pkgs \
|
||||||
|
--no-remove \
|
||||||
|
upgrade
|
||||||
|
```
|
||||||
|
|
||||||
|
Почему:
|
||||||
|
|
||||||
|
- `upgrade`, не `dist-upgrade`/`full-upgrade`: пакеты не удаляются;
|
||||||
|
- `--with-new-pkgs`: разрешает новые зависимости, включая новое ядро;
|
||||||
|
- `--no-remove`: аварийно останавливает операцию, если APT захочет удалить
|
||||||
|
пакет;
|
||||||
|
- `--force-confold`: сохраняет локальные конфиги вместо тихой замены;
|
||||||
|
- `NEEDRESTART_MODE=l`: показывает сервисы со старыми библиотеками, но не
|
||||||
|
перезапускает все сервисы автоматически.
|
||||||
|
|
||||||
|
Пакетные maintainer scripts все равно могут перезапустить собственный сервис.
|
||||||
|
Абсолютно гарантировать отсутствие краткого service interruption при системном
|
||||||
|
обновлении невозможно.
|
||||||
|
|
||||||
|
### 7. Не удаляет зависимости автоматически
|
||||||
|
|
||||||
|
По умолчанию выполняется только dry-run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
apt-get --dry-run autoremove
|
||||||
|
```
|
||||||
|
|
||||||
|
Кандидаты попадают в журнал. Реальный `autoremove` включается только вручную
|
||||||
|
через конфигурацию.
|
||||||
|
|
||||||
|
### 8. Не перезагружает сервер
|
||||||
|
|
||||||
|
Скрипт проверяет `/var/run/reboot-required` и сравнивает текущее ядро с
|
||||||
|
`/vmlinuz`. При необходимости пишет:
|
||||||
|
|
||||||
|
```text
|
||||||
|
REBOOT REQUIRED. Automatic reboot disabled.
|
||||||
|
```
|
||||||
|
|
||||||
|
Решение о reboot остается у администратора.
|
||||||
|
|
||||||
|
### 9. Показывает итог
|
||||||
|
|
||||||
|
В конце журнал содержит:
|
||||||
|
|
||||||
|
- failed systemd units;
|
||||||
|
- использование диска;
|
||||||
|
- необходимость reboot;
|
||||||
|
- успешные и неуспешные этапы.
|
||||||
|
|
||||||
|
## Настройка
|
||||||
|
|
||||||
|
Файл:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo nano /etc/default/safe-monthly-maintenance
|
||||||
|
```
|
||||||
|
|
||||||
|
Параметры:
|
||||||
|
|
||||||
|
| Параметр | По умолчанию | Значение |
|
||||||
|
|---|---:|---|
|
||||||
|
| `ENABLE_AUTOREMOVE` | `0` | `1` разрешает `apt-get autoremove --purge` |
|
||||||
|
| `ENABLE_DOCKER_PRUNE` | `0` | `1` удаляет только dangling Docker images |
|
||||||
|
| `JOURNAL_MAX_AGE` | `30d` | Максимальный возраст systemd journal |
|
||||||
|
| `JOURNAL_MAX_SIZE` | `500M` | Максимальный размер journal |
|
||||||
|
| `MIN_ROOT_FREE_KB` | `1048576` | Минимум свободного места на `/` |
|
||||||
|
| `MIN_BOOT_FREE_KB` | `204800` | Минимум на отдельном `/boot` |
|
||||||
|
|
||||||
|
Для серверов с важными stopped-контейнерами и локальными Docker-образами
|
||||||
|
оставляйте `ENABLE_DOCKER_PRUNE=0`.
|
||||||
|
|
||||||
|
## Управление
|
||||||
|
|
||||||
|
Следующий запуск:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl list-timers safe-monthly-maintenance.timer --all
|
||||||
|
```
|
||||||
|
|
||||||
|
Запустить вручную:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl start safe-monthly-maintenance.service
|
||||||
|
```
|
||||||
|
|
||||||
|
Последний статус:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl status safe-monthly-maintenance.service
|
||||||
|
```
|
||||||
|
|
||||||
|
Логи:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
journalctl -u safe-monthly-maintenance.service
|
||||||
|
```
|
||||||
|
|
||||||
|
Логи текущего запуска:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
journalctl -u safe-monthly-maintenance.service --since today
|
||||||
|
```
|
||||||
|
|
||||||
|
Отключить автоматический запуск:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl disable --now safe-monthly-maintenance.timer
|
||||||
|
```
|
||||||
|
|
||||||
|
Удалить:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl disable --now safe-monthly-maintenance.timer
|
||||||
|
sudo rm -f /etc/systemd/system/safe-monthly-maintenance.timer
|
||||||
|
sudo rm -f /etc/systemd/system/safe-monthly-maintenance.service
|
||||||
|
sudo rm -f /usr/local/sbin/safe-monthly-maintenance
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
```
|
||||||
|
|
||||||
|
Конфигурация и package snapshots при удалении сохраняются намеренно.
|
||||||
|
|
||||||
|
## Массовое развертывание
|
||||||
|
|
||||||
|
Пример с локальным списком серверов:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
for host in vm1.example.com vm2.example.com vm3.example.com; do
|
||||||
|
ssh "$host" \
|
||||||
|
'curl -fsSL https://raw.githubusercontent.com/andrey271192/safe-monthly-maintenance/v1.0.2/install.sh | sudo bash -s -- --run-now'
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
Сначала используйте одну тестовую VM. Для большой инфраструктуры лучше Ansible,
|
||||||
|
Salt или другой configuration-management инструмент.
|
||||||
|
|
||||||
|
## Ограничения
|
||||||
|
|
||||||
|
- Скрипт не делает filesystem/provider snapshot.
|
||||||
|
- Скрипт не исправляет сломанный `dpkg` автоматически.
|
||||||
|
- Скрипт не обновляет Ubuntu/Debian между major releases.
|
||||||
|
- Скрипт не управляет сторонними package managers.
|
||||||
|
- Скрипт не гарантирует отсутствие service restart от maintainer scripts.
|
||||||
|
- Скрипт не заменяет мониторинг, резервные копии и тестирование обновлений.
|
||||||
|
|
||||||
|
Подробная модель безопасности: [docs/SAFETY.md](docs/SAFETY.md).
|
||||||
|
|
||||||
|
## Лицензия
|
||||||
|
|
||||||
|
[MIT](LICENSE)
|
||||||
24
SECURITY.md
Normal file
24
SECURITY.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Reporting
|
||||||
|
|
||||||
|
Не публикуйте токены, пароли, SSH-ключи, приватные IP-схемы или конфигурации
|
||||||
|
серверов в issue.
|
||||||
|
|
||||||
|
Для потенциальной уязвимости используйте GitHub Private Vulnerability Reporting,
|
||||||
|
если функция доступна для репозитория.
|
||||||
|
|
||||||
|
## Running as root
|
||||||
|
|
||||||
|
Installer и maintenance-скрипт работают с правами `root`, потому что APT,
|
||||||
|
systemd и journal требуют системного доступа.
|
||||||
|
|
||||||
|
Перед запуском:
|
||||||
|
|
||||||
|
1. используйте URL закрепленного release tag;
|
||||||
|
2. проверяйте SHA-256;
|
||||||
|
3. просматривайте скачанный файл;
|
||||||
|
4. тестируйте на отдельной VM;
|
||||||
|
5. имейте snapshot или резервную копию.
|
||||||
|
|
||||||
|
Никогда не подставляйте секреты в URL installer-команды.
|
||||||
82
docs/SAFETY.md
Normal file
82
docs/SAFETY.md
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
# Модель безопасности
|
||||||
|
|
||||||
|
Цель проекта — выполнять повторяемое обслуживание Ubuntu/Debian с минимальным
|
||||||
|
риском. «Безопасно» не означает «невозможно сломать»: любое системное обновление
|
||||||
|
может содержать ошибку пакета, несовместимость приложения или service restart.
|
||||||
|
|
||||||
|
## Принципы
|
||||||
|
|
||||||
|
### Без удаления пакетов
|
||||||
|
|
||||||
|
Основное обновление использует `apt-get upgrade --no-remove`.
|
||||||
|
`dist-upgrade` и `full-upgrade` не используются.
|
||||||
|
|
||||||
|
`autoremove` выключен. Причина: пакет, нужный локальному приложению, иногда
|
||||||
|
ошибочно остается помеченным как автоматически установленный.
|
||||||
|
|
||||||
|
### Без автоматического reboot
|
||||||
|
|
||||||
|
Новое ядро устанавливается, но активируется только после решения администратора.
|
||||||
|
Это предотвращает незапланированный разрыв VPN, базы данных, SSH или другого
|
||||||
|
сервиса.
|
||||||
|
|
||||||
|
### Без опасной очистки файлов
|
||||||
|
|
||||||
|
Очистка временных данных делегируется `systemd-tmpfiles`, `logrotate`,
|
||||||
|
`journalctl` и APT. Проект не содержит рекурсивного удаления широких каталогов.
|
||||||
|
|
||||||
|
### Без очистки Docker по умолчанию
|
||||||
|
|
||||||
|
Stopped containers, tagged images, volumes и networks могут содержать нужное
|
||||||
|
состояние. Поэтому Docker cleanup выключен. Опция удаляет только dangling images.
|
||||||
|
|
||||||
|
### Сохранение конфигов
|
||||||
|
|
||||||
|
`Dpkg::Options::=--force-confold` сохраняет установленную локальную конфигурацию.
|
||||||
|
Новая версия файла пакета обычно остается рядом как `.dpkg-dist`; ее нужно
|
||||||
|
сравнить вручную.
|
||||||
|
|
||||||
|
### Проверки до изменения
|
||||||
|
|
||||||
|
APT upgrade не запускается при:
|
||||||
|
|
||||||
|
- поврежденном/незавершенном состоянии `dpkg`;
|
||||||
|
- критически малом свободном месте;
|
||||||
|
- ошибке `apt-get update`.
|
||||||
|
|
||||||
|
### Один процесс
|
||||||
|
|
||||||
|
`flock` предотвращает параллельные maintenance-запуски. APT-lock ожидается до
|
||||||
|
10 минут, что уменьшает конфликт с `apt-daily`.
|
||||||
|
|
||||||
|
## Рекомендуемая эксплуатация
|
||||||
|
|
||||||
|
1. Создайте snapshot VM перед первым запуском.
|
||||||
|
2. Проверьте installer и checksum.
|
||||||
|
3. Запустите на тестовой VM.
|
||||||
|
4. Проверьте journal, failed units и приложение.
|
||||||
|
5. Только потом развертывайте на остальные серверы.
|
||||||
|
6. Перезагружайте сервер в согласованное maintenance window.
|
||||||
|
|
||||||
|
## Что проверить после запуска
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl --failed
|
||||||
|
dpkg --audit
|
||||||
|
apt list --upgradable
|
||||||
|
journalctl -u safe-monthly-maintenance.service -n 200
|
||||||
|
test -e /var/run/reboot-required && echo "reboot required"
|
||||||
|
```
|
||||||
|
|
||||||
|
Для VPN, reverse proxy, базы данных и других сервисов добавьте собственные
|
||||||
|
health checks после системного обслуживания.
|
||||||
|
|
||||||
|
## Rollback
|
||||||
|
|
||||||
|
Проект сохраняет список пакетов, но не создает полноценный rollback.
|
||||||
|
|
||||||
|
Для надежного возврата нужны:
|
||||||
|
|
||||||
|
- snapshot гипервизора/VPS-провайдера;
|
||||||
|
- резервная копия `/etc` и данных приложений;
|
||||||
|
- отдельно проверенная процедура восстановления.
|
||||||
85
docs/TROUBLESHOOTING.md
Normal file
85
docs/TROUBLESHOOTING.md
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
# Диагностика
|
||||||
|
|
||||||
|
## Service завершился с ошибкой
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl status safe-monthly-maintenance.service
|
||||||
|
journalctl -u safe-monthly-maintenance.service -n 300 --no-pager
|
||||||
|
```
|
||||||
|
|
||||||
|
Скрипт продолжает независимые безопасные этапы и в конце возвращает ненулевой
|
||||||
|
код, если хотя бы один этап завершился ошибкой.
|
||||||
|
|
||||||
|
## APT занят
|
||||||
|
|
||||||
|
Скрипт ожидает lock до 600 секунд. Найдите владельца lock:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ps aux | grep -E 'apt|dpkg'
|
||||||
|
systemctl status apt-daily.service apt-daily-upgrade.service
|
||||||
|
```
|
||||||
|
|
||||||
|
Не удаляйте lock-файлы вручную, пока работает APT/DPKG.
|
||||||
|
|
||||||
|
## `dpkg --audit` показывает проблему
|
||||||
|
|
||||||
|
Проверьте вывод:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo dpkg --audit
|
||||||
|
```
|
||||||
|
|
||||||
|
Типовой ремонт:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo dpkg --configure -a
|
||||||
|
sudo apt-get -f install
|
||||||
|
```
|
||||||
|
|
||||||
|
Читайте список изменений перед подтверждением.
|
||||||
|
|
||||||
|
## Недостаточно места
|
||||||
|
|
||||||
|
```bash
|
||||||
|
df -h / /boot
|
||||||
|
du -xhd1 /var | sort -h
|
||||||
|
journalctl --disk-usage
|
||||||
|
```
|
||||||
|
|
||||||
|
Не уменьшайте защитный порог без понимания причины заполнения.
|
||||||
|
|
||||||
|
## Требуется reboot
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uname -r
|
||||||
|
readlink -f /vmlinuz
|
||||||
|
cat /var/run/reboot-required 2>/dev/null
|
||||||
|
```
|
||||||
|
|
||||||
|
Перезагрузите в maintenance window:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl reboot
|
||||||
|
```
|
||||||
|
|
||||||
|
После reboot:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uname -r
|
||||||
|
systemctl --failed
|
||||||
|
```
|
||||||
|
|
||||||
|
## Timer не запускается
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl is-enabled safe-monthly-maintenance.timer
|
||||||
|
systemctl is-active safe-monthly-maintenance.timer
|
||||||
|
systemctl list-timers safe-monthly-maintenance.timer --all
|
||||||
|
```
|
||||||
|
|
||||||
|
Восстановить:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable --now safe-monthly-maintenance.timer
|
||||||
|
```
|
||||||
379
install.sh
Executable file
379
install.sh
Executable file
@@ -0,0 +1,379 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
INSTALLER_VERSION="1.0.2"
|
||||||
|
SCRIPT_PATH="/usr/local/sbin/safe-monthly-maintenance"
|
||||||
|
CONFIG_PATH="/etc/default/safe-monthly-maintenance"
|
||||||
|
SERVICE_PATH="/etc/systemd/system/safe-monthly-maintenance.service"
|
||||||
|
TIMER_PATH="/etc/systemd/system/safe-monthly-maintenance.timer"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage:
|
||||||
|
sudo bash install-safe-monthly-maintenance.sh [--run-now]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--run-now Install timer, then run maintenance immediately.
|
||||||
|
--help Show help.
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
RUN_NOW=0
|
||||||
|
case "${1:-}" in
|
||||||
|
"")
|
||||||
|
;;
|
||||||
|
--run-now)
|
||||||
|
RUN_NOW=1
|
||||||
|
;;
|
||||||
|
--help|-h)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown option: $1" >&2
|
||||||
|
usage >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if (( EUID != 0 )); then
|
||||||
|
echo "Run as root: sudo bash $0 ${1:-}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -r /etc/os-release ]]; then
|
||||||
|
echo "Unsupported system: /etc/os-release missing" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
. /etc/os-release
|
||||||
|
case "${ID:-}:${ID_LIKE:-}" in
|
||||||
|
ubuntu:*|debian:*|*:*debian*)
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unsupported distribution: ${PRETTY_NAME:-unknown}. Ubuntu/Debian required." >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
for command_name in apt-get dpkg systemctl systemd-analyze journalctl flock systemd-tmpfiles; do
|
||||||
|
if ! command -v "$command_name" >/dev/null 2>&1; then
|
||||||
|
echo "Required command missing: $command_name" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
work_dir="$(mktemp -d)"
|
||||||
|
cleanup() {
|
||||||
|
rm -rf -- "$work_dir"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
cat >"$work_dir/safe-monthly-maintenance" <<'MAINTENANCE_SCRIPT'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
umask 027
|
||||||
|
|
||||||
|
MAINTENANCE_VERSION="1.0.2"
|
||||||
|
CONFIG_PATH="/etc/default/safe-monthly-maintenance"
|
||||||
|
LOCK_PATH="/run/lock/safe-monthly-maintenance.lock"
|
||||||
|
BACKUP_DIR="/var/backups/safe-monthly-maintenance"
|
||||||
|
|
||||||
|
# Safe defaults. Override through /etc/default/safe-monthly-maintenance.
|
||||||
|
JOURNAL_MAX_AGE="30d"
|
||||||
|
JOURNAL_MAX_SIZE="500M"
|
||||||
|
MIN_ROOT_FREE_KB="1048576"
|
||||||
|
MIN_BOOT_FREE_KB="204800"
|
||||||
|
ENABLE_AUTOREMOVE="0"
|
||||||
|
ENABLE_DOCKER_PRUNE="0"
|
||||||
|
|
||||||
|
case "${1:-}" in
|
||||||
|
"")
|
||||||
|
;;
|
||||||
|
--help|-h)
|
||||||
|
cat <<'EOF'
|
||||||
|
Safe monthly Ubuntu/Debian maintenance.
|
||||||
|
|
||||||
|
Configuration: /etc/default/safe-monthly-maintenance
|
||||||
|
Logs: journalctl -u safe-monthly-maintenance.service
|
||||||
|
|
||||||
|
No automatic reboot. Autoremove and Docker prune are disabled by default.
|
||||||
|
EOF
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown option: $1" >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [[ -r "$CONFIG_PATH" ]]; then
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
. "$CONFIG_PATH"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log() {
|
||||||
|
printf '[safe-maintenance] %s\n' "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
failures=()
|
||||||
|
run_step() {
|
||||||
|
local label="$1"
|
||||||
|
local rc
|
||||||
|
shift
|
||||||
|
log "START: $label"
|
||||||
|
if "$@"; then
|
||||||
|
log "OK: $label"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
rc=$?
|
||||||
|
failures+=("$label (exit $rc)")
|
||||||
|
log "ERROR: $label (exit $rc)"
|
||||||
|
return "$rc"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
if (( EUID != 0 )); then
|
||||||
|
log "ERROR: root privileges required"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec 9>"$LOCK_PATH"
|
||||||
|
if ! flock -n 9; then
|
||||||
|
log "Another maintenance run is active. Exiting."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -r /etc/os-release ]]; then
|
||||||
|
log "ERROR: /etc/os-release missing"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
. /etc/os-release
|
||||||
|
case "${ID:-}:${ID_LIKE:-}" in
|
||||||
|
ubuntu:*|debian:*|*:*debian*)
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
log "ERROR: unsupported distribution ${PRETTY_NAME:-unknown}"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
export APT_LISTCHANGES_FRONTEND=none
|
||||||
|
# List services using old libraries; never restart every service automatically.
|
||||||
|
export NEEDRESTART_MODE=l
|
||||||
|
|
||||||
|
APT_OPTIONS=(
|
||||||
|
-o Dpkg::Lock::Timeout=600
|
||||||
|
-o Acquire::Retries=3
|
||||||
|
)
|
||||||
|
|
||||||
|
log "Version $MAINTENANCE_VERSION"
|
||||||
|
log "System: ${PRETTY_NAME:-unknown}"
|
||||||
|
log "Started: $(date --iso-8601=seconds)"
|
||||||
|
|
||||||
|
install -d -m 0750 "$BACKUP_DIR"
|
||||||
|
snapshot_path="$BACKUP_DIR/dpkg-selections-$(date +%Y%m%d-%H%M%S).txt"
|
||||||
|
if dpkg --get-selections >"$snapshot_path"; then
|
||||||
|
log "Package selection snapshot: $snapshot_path"
|
||||||
|
else
|
||||||
|
failures+=("package selection snapshot")
|
||||||
|
rm -f -- "$snapshot_path"
|
||||||
|
fi
|
||||||
|
find "$BACKUP_DIR" -maxdepth 1 -type f -name 'dpkg-selections-*.txt' -mtime +120 -delete
|
||||||
|
|
||||||
|
# Use system policies for temporary files and logs. No manual rm in /tmp or /var/log.
|
||||||
|
run_step "systemd temporary-file cleanup" systemd-tmpfiles --clean || true
|
||||||
|
if command -v logrotate >/dev/null 2>&1; then
|
||||||
|
run_step "scheduled log rotation" logrotate /etc/logrotate.conf || true
|
||||||
|
fi
|
||||||
|
run_step "journal rotation" journalctl --rotate || true
|
||||||
|
run_step "journal age cleanup" journalctl "--vacuum-time=$JOURNAL_MAX_AGE" || true
|
||||||
|
run_step "journal size cleanup" journalctl "--vacuum-size=$JOURNAL_MAX_SIZE" || true
|
||||||
|
|
||||||
|
if ! dpkg_audit="$(dpkg --audit 2>&1)"; then
|
||||||
|
failures+=("dpkg audit command")
|
||||||
|
log "ERROR: dpkg audit failed: $dpkg_audit"
|
||||||
|
apt_ready=0
|
||||||
|
elif [[ -n "$dpkg_audit" ]]; then
|
||||||
|
failures+=("dpkg state incomplete")
|
||||||
|
log "ERROR: interrupted/incomplete package state detected:"
|
||||||
|
printf '%s\n' "$dpkg_audit"
|
||||||
|
log "APT upgrade skipped. Repair manually with: dpkg --configure -a"
|
||||||
|
apt_ready=0
|
||||||
|
else
|
||||||
|
apt_ready=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
run_step "APT autoclean" apt-get "${APT_OPTIONS[@]}" -y autoclean || true
|
||||||
|
|
||||||
|
if (( apt_ready == 1 )); then
|
||||||
|
if run_step "APT metadata update" apt-get "${APT_OPTIONS[@]}" update; then
|
||||||
|
root_free_kb="$(df -Pk / | awk 'NR == 2 {print $4}')"
|
||||||
|
log "Root free space: ${root_free_kb} KiB"
|
||||||
|
|
||||||
|
space_ok=1
|
||||||
|
if (( root_free_kb < MIN_ROOT_FREE_KB )); then
|
||||||
|
failures+=("insufficient root free space")
|
||||||
|
log "ERROR: root free space below ${MIN_ROOT_FREE_KB} KiB; upgrade skipped"
|
||||||
|
space_ok=0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if mountpoint -q /boot; then
|
||||||
|
boot_free_kb="$(df -Pk /boot | awk 'NR == 2 {print $4}')"
|
||||||
|
log "/boot free space: ${boot_free_kb} KiB"
|
||||||
|
if (( boot_free_kb < MIN_BOOT_FREE_KB )); then
|
||||||
|
failures+=("insufficient /boot free space")
|
||||||
|
log "ERROR: /boot free space below ${MIN_BOOT_FREE_KB} KiB; upgrade skipped"
|
||||||
|
space_ok=0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if (( space_ok == 1 )); then
|
||||||
|
# Never use dist-upgrade/full-upgrade. Never remove packages.
|
||||||
|
# --with-new-pkgs permits kernel/security dependencies without removals.
|
||||||
|
run_step "safe package upgrade" \
|
||||||
|
apt-get "${APT_OPTIONS[@]}" -y \
|
||||||
|
-o Dpkg::Options::=--force-confold \
|
||||||
|
--with-new-pkgs --no-remove upgrade || true
|
||||||
|
run_step "APT consistency check" apt-get "${APT_OPTIONS[@]}" check || true
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log "APT upgrade skipped because metadata update failed"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$ENABLE_AUTOREMOVE" == "1" ]]; then
|
||||||
|
log "Autoremove explicitly enabled by config"
|
||||||
|
run_step "APT autoremove" \
|
||||||
|
apt-get "${APT_OPTIONS[@]}" -y --purge autoremove || true
|
||||||
|
else
|
||||||
|
log "Autoremove disabled. Dry-run candidates:"
|
||||||
|
apt-get "${APT_OPTIONS[@]}" --dry-run autoremove || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
run_step "APT cache cleanup" apt-get "${APT_OPTIONS[@]}" clean || true
|
||||||
|
|
||||||
|
if [[ "$ENABLE_DOCKER_PRUNE" == "1" ]] && command -v docker >/dev/null 2>&1; then
|
||||||
|
log "Docker dangling-image prune explicitly enabled by config"
|
||||||
|
run_step "Docker dangling-image prune" docker image prune -f || true
|
||||||
|
else
|
||||||
|
log "Docker prune disabled"
|
||||||
|
fi
|
||||||
|
|
||||||
|
reboot_required=0
|
||||||
|
if [[ -e /var/run/reboot-required ]]; then
|
||||||
|
reboot_required=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
running_kernel="$(uname -r)"
|
||||||
|
latest_kernel=""
|
||||||
|
if [[ -e /vmlinuz ]]; then
|
||||||
|
latest_kernel="$(basename "$(readlink -f /vmlinuz)")"
|
||||||
|
latest_kernel="${latest_kernel#vmlinuz-}"
|
||||||
|
fi
|
||||||
|
if [[ -n "$latest_kernel" && "$latest_kernel" != "$running_kernel" ]]; then
|
||||||
|
reboot_required=1
|
||||||
|
log "Installed kernel differs from running kernel: $running_kernel -> $latest_kernel"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if (( reboot_required == 1 )); then
|
||||||
|
log "REBOOT REQUIRED. Automatic reboot disabled."
|
||||||
|
if [[ -r /var/run/reboot-required.pkgs ]]; then
|
||||||
|
sed 's/^/[safe-maintenance] reboot package: /' /var/run/reboot-required.pkgs
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
log "Reboot not required"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Failed systemd units:"
|
||||||
|
systemctl --failed --no-pager || true
|
||||||
|
log "Disk usage:"
|
||||||
|
df -h / /boot 2>/dev/null | awk '!seen[$1]++' || true
|
||||||
|
log "Finished: $(date --iso-8601=seconds)"
|
||||||
|
|
||||||
|
if (( ${#failures[@]} > 0 )); then
|
||||||
|
log "Completed with ${#failures[@]} warning/error(s):"
|
||||||
|
printf '[safe-maintenance] - %s\n' "${failures[@]}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Completed successfully"
|
||||||
|
MAINTENANCE_SCRIPT
|
||||||
|
|
||||||
|
cat >"$work_dir/safe-monthly-maintenance.default" <<'MAINTENANCE_CONFIG'
|
||||||
|
# Safe defaults. Set to 1 only after reviewing each VM.
|
||||||
|
ENABLE_AUTOREMOVE=0
|
||||||
|
ENABLE_DOCKER_PRUNE=0
|
||||||
|
|
||||||
|
# Journal retention limits.
|
||||||
|
JOURNAL_MAX_AGE=30d
|
||||||
|
JOURNAL_MAX_SIZE=500M
|
||||||
|
|
||||||
|
# Skip package upgrade below these free-space thresholds.
|
||||||
|
MIN_ROOT_FREE_KB=1048576
|
||||||
|
MIN_BOOT_FREE_KB=204800
|
||||||
|
MAINTENANCE_CONFIG
|
||||||
|
|
||||||
|
cat >"$work_dir/safe-monthly-maintenance.service" <<'SERVICE_UNIT'
|
||||||
|
[Unit]
|
||||||
|
Description=Safe monthly Ubuntu/Debian maintenance
|
||||||
|
Documentation=man:apt-get(8)
|
||||||
|
Wants=network-online.target
|
||||||
|
After=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=/usr/local/sbin/safe-monthly-maintenance
|
||||||
|
Nice=10
|
||||||
|
IOSchedulingClass=best-effort
|
||||||
|
IOSchedulingPriority=7
|
||||||
|
TimeoutStartSec=2h
|
||||||
|
SERVICE_UNIT
|
||||||
|
|
||||||
|
cat >"$work_dir/safe-monthly-maintenance.timer" <<'TIMER_UNIT'
|
||||||
|
[Unit]
|
||||||
|
Description=Run safe monthly Ubuntu/Debian maintenance
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnCalendar=*-*-01 03:00:00
|
||||||
|
RandomizedDelaySec=6h
|
||||||
|
Persistent=true
|
||||||
|
AccuracySec=15m
|
||||||
|
Unit=safe-monthly-maintenance.service
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=timers.target
|
||||||
|
TIMER_UNIT
|
||||||
|
|
||||||
|
bash -n "$work_dir/safe-monthly-maintenance"
|
||||||
|
install -m 0755 "$work_dir/safe-monthly-maintenance" "$SCRIPT_PATH"
|
||||||
|
systemd-analyze verify \
|
||||||
|
"$work_dir/safe-monthly-maintenance.service" \
|
||||||
|
"$work_dir/safe-monthly-maintenance.timer"
|
||||||
|
|
||||||
|
if [[ ! -e "$CONFIG_PATH" ]]; then
|
||||||
|
install -m 0644 "$work_dir/safe-monthly-maintenance.default" "$CONFIG_PATH"
|
||||||
|
else
|
||||||
|
echo "Preserving existing config: $CONFIG_PATH"
|
||||||
|
fi
|
||||||
|
install -m 0644 "$work_dir/safe-monthly-maintenance.service" "$SERVICE_PATH"
|
||||||
|
install -m 0644 "$work_dir/safe-monthly-maintenance.timer" "$TIMER_PATH"
|
||||||
|
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable --now safe-monthly-maintenance.timer
|
||||||
|
|
||||||
|
echo "Installed safe monthly maintenance v$INSTALLER_VERSION"
|
||||||
|
systemctl list-timers safe-monthly-maintenance.timer --all --no-pager
|
||||||
|
echo "Config: $CONFIG_PATH"
|
||||||
|
echo "Logs: journalctl -u safe-monthly-maintenance.service"
|
||||||
|
|
||||||
|
if (( RUN_NOW == 1 )); then
|
||||||
|
echo "Running maintenance now..."
|
||||||
|
systemctl reset-failed safe-monthly-maintenance.service 2>/dev/null || true
|
||||||
|
systemctl start safe-monthly-maintenance.service
|
||||||
|
systemctl status safe-monthly-maintenance.service --no-pager -l
|
||||||
|
fi
|
||||||
Reference in New Issue
Block a user