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