Skip to content

Instantly share code, notes, and snippets.

@hightemp
Created April 7, 2026 07:16
Show Gist options
  • Select an option

  • Save hightemp/dd129bcdfec012744e5cfa2f380d197d to your computer and use it in GitHub Desktop.

Select an option

Save hightemp/dd129bcdfec012744e5cfa2f380d197d to your computer and use it in GitHub Desktop.
Скрипт-помощник для обновления ubuntu с 20 до 24
#!/usr/bin/env bash
# Ubuntu 20.04 → 22.04 → 24.04 upgrade script
# Идемпотентный: запускай повторно после каждой перезагрузки
set -euo pipefail
# ─── Константы ───────────────────────────────────────────────────────────────
STATE_DIR="/var/lib/ubuntu-upgrade"
STATE_FILE="$STATE_DIR/state"
PPA_BACKUP_DIR="$STATE_DIR/ppa_backup"
LOG_DIR="/var/log/ubuntu-upgrade"
LOG_FILE="$LOG_DIR/upgrade.log"
SCRIPT_PATH="$(realpath "$0")"
# ─── Цвета ───────────────────────────────────────────────────────────────────
RED='\033[0;31m'
GRN='\033[0;32m'
YLW='\033[1;33m'
BLU='\033[0;34m'
CYN='\033[0;36m'
BLD='\033[1m'
RST='\033[0m'
# ─── Утилиты вывода ──────────────────────────────────────────────────────────
info() { echo -e "${BLU}[INFO]${RST} $*" | tee -a "$LOG_FILE"; }
ok() { echo -e "${GRN}[OK]${RST} $*" | tee -a "$LOG_FILE"; }
warn() { echo -e "${YLW}[WARN]${RST} $*" | tee -a "$LOG_FILE"; }
error() { echo -e "${RED}[ERROR]${RST} $*" | tee -a "$LOG_FILE"; }
step() {
echo -e "\n${BLD}${CYN}══════════════════════════════════════${RST}" | tee -a "$LOG_FILE"
echo -e "${BLD}${CYN} $*${RST}" | tee -a "$LOG_FILE"
echo -e "${BLD}${CYN}══════════════════════════════════════${RST}" | tee -a "$LOG_FILE"
}
die() { error "$*"; error "Лог: $LOG_FILE"; exit 1; }
# ─── Инициализация ───────────────────────────────────────────────────────────
init_dirs() {
mkdir -p "$STATE_DIR" "$PPA_BACKUP_DIR" "$LOG_DIR"
touch "$LOG_FILE"
{
echo "──────────────────────────────────────────"
echo "Запуск: $(date '+%Y-%m-%d %H:%M:%S')"
echo "Ubuntu: $(lsb_release -ds 2>/dev/null || cat /etc/os-release | grep PRETTY | cut -d= -f2)"
echo "──────────────────────────────────────────"
} >> "$LOG_FILE"
}
# ─── Состояние ───────────────────────────────────────────────────────────────
get_state() {
[[ -f "$STATE_FILE" ]] && cat "$STATE_FILE" || echo "start"
}
set_state() {
echo "$1" > "$STATE_FILE"
info "Состояние сохранено: $1"
}
# ─── Версия Ubuntu ───────────────────────────────────────────────────────────
get_ubuntu_version() {
# shellcheck disable=SC1091
. /etc/os-release
echo "$VERSION_ID"
}
# ─── Проверка прав ───────────────────────────────────────────────────────────
check_root() {
if [[ $EUID -ne 0 ]]; then
echo -e "${RED}Запусти скрипт с sudo:${RST} sudo bash $SCRIPT_PATH"
exit 1
fi
}
# ─── Предполётные проверки ───────────────────────────────────────────────────
preflight_checks() {
local target_version="$1"
step "Предполётные проверки перед апгрейдом до Ubuntu $target_version"
local errors=0
info "Текущая версия: Ubuntu $(get_ubuntu_version)"
# Питание (ноутбук)
local battery_path=""
for p in /sys/class/power_supply/BAT*; do
[[ -e "$p" ]] && battery_path="$p" && break
done
if [[ -n "$battery_path" ]]; then
local status capacity
status=$(cat "$battery_path/status" 2>/dev/null || echo "Unknown")
capacity=$(cat "$battery_path/capacity" 2>/dev/null || echo "0")
if [[ "$status" != "Charging" && "$status" != "Full" ]]; then
if (( capacity < 50 )); then
error "🔋 Заряд батареи: ${capacity}% — недостаточно для апгрейда!"
error " Подключи зарядное устройство и запусти скрипт снова."
(( errors++ )) || true
else
warn "🔋 Батарея не заряжается (${capacity}%). Рекомендуется подключить зарядку."
fi
else
ok "🔋 Питание: $status (${capacity}%)"
fi
else
info "🔋 Батарея не обнаружена (возможно, стационарный ПК или ноутбук с полным зарядом)"
fi
# Место на диске /
local root_free_mb
root_free_mb=$(df -BM / | awk 'NR==2{gsub(/M/,"",$4); print $4}')
if (( root_free_mb < 5120 )); then
error "💾 Мало места на /: ${root_free_mb} МБ (нужно минимум 5 ГБ)"
error " Освободи место:"
error " sudo apt autoremove --purge"
error " sudo apt clean"
error " sudo journalctl --vacuum-size=200M"
(( errors++ )) || true
else
ok "💾 Место на /: ${root_free_mb} МБ — достаточно"
fi
# Место на /boot
local boot_free_mb=999
if mountpoint -q /boot 2>/dev/null; then
boot_free_mb=$(df -BM /boot | awk 'NR==2{gsub(/M/,"",$4); print $4}')
fi
if (( boot_free_mb < 200 )); then
error "💾 Мало места в /boot: ${boot_free_mb} МБ (нужно >200 МБ)"
error " Удали старые ядра:"
error " dpkg -l | grep 'linux-image' # посмотреть ядра"
error " sudo apt autoremove --purge # автоудаление"
(( errors++ )) || true
else
ok "💾 Место в /boot: ${boot_free_mb} МБ"
fi
# dpkg — нет незавершённых операций
local dpkg_audit
dpkg_audit=$(dpkg --audit 2>&1 || true)
if [[ -n "$dpkg_audit" ]]; then
error "⚙️ Есть проблемные пакеты в dpkg:"
echo "$dpkg_audit" | tee -a "$LOG_FILE"
error " Исправь:"
error " sudo dpkg --configure -a"
error " sudo apt --fix-broken install"
(( errors++ )) || true
else
ok "⚙️ dpkg: нет незавершённых операций"
fi
# Удержанные пакеты
local held
held=$(apt-mark showhold 2>/dev/null || true)
if [[ -n "$held" ]]; then
warn "📌 Удержанные пакеты (held): $held"
warn " Рекомендуется снять: sudo apt-mark unhold <пакет>"
else
ok "📌 Удержанных пакетов нет"
fi
# Сторонние PPA
local ppa_count
ppa_count=$(find /etc/apt/sources.list.d/ -name "*.list" 2>/dev/null | wc -l)
if (( ppa_count > 0 )); then
warn "📋 Сторонних репозиториев: $ppa_count — будут отключены перед апгрейдом"
else
ok "📋 Сторонних репозиториев нет"
fi
# SSH-предупреждение
if [[ -n "${SSH_CONNECTION:-}" ]]; then
warn "🌐 Запущено по SSH — убедись, что есть запасной доступ!"
warn " Рекомендуется: tmux new -s upgrade"
fi
echo "" | tee -a "$LOG_FILE"
if (( errors > 0 )); then
die "Найдено критических проблем: $errors. Исправь их и запусти скрипт снова."
fi
ok "Все предполётные проверки пройдены ✓"
}
# ─── PPA management ──────────────────────────────────────────────────────────
disable_ppas() {
step "Отключение сторонних репозиториев"
local count=0
while IFS= read -r -d '' f; do
cp "$f" "$PPA_BACKUP_DIR/$(basename "$f")"
mv "$f" "${f}.disabled"
info "Отключён: $(basename "$f")"
(( count++ )) || true
done < <(find /etc/apt/sources.list.d/ -name "*.list" -print0 2>/dev/null)
if (( count > 0 )); then
ok "Отключено PPA: $count (бэкап: $PPA_BACKUP_DIR)"
else
ok "Сторонних репозиториев не было"
fi
apt-get update -qq 2>&1 | tee -a "$LOG_FILE" || true
}
restore_ppas() {
step "Восстановление сторонних репозиториев"
local restored=0
while IFS= read -r -d '' f; do
mv "$f" "${f%.disabled}"
info "Восстановлен: $(basename "${f%.disabled}")"
(( restored++ )) || true
done < <(find /etc/apt/sources.list.d/ -name "*.disabled" -print0 2>/dev/null)
if (( restored > 0 )); then
warn "Восстановлено PPA: $restored"
warn "Проверь совместимость каждого PPA с новой версией Ubuntu!"
warn "Если после apt update есть ошибки — отключи проблемный PPA:"
warn " sudo add-apt-repository --remove ppa:user/repo"
apt-get update 2>&1 | tee -a "$LOG_FILE" || \
warn "apt update выдал предупреждения после восстановления PPA — проверь вручную"
else
ok "PPA для восстановления не найдены"
fi
}
# ─── Timeshift снапшот ───────────────────────────────────────────────────────
take_snapshot() {
local label="$1"
if command -v timeshift &>/dev/null; then
step "Создание снапшота Timeshift: $label"
if timeshift --create --comments "$label" --yes 2>&1 | tee -a "$LOG_FILE"; then
ok "Снапшот создан: $label"
else
warn "Timeshift не смог создать снапшот — возможно, не настроен."
warn "Продолжаем без снапшота. Рекомендуется вручную сохранить важные данные."
fi
else
warn "Timeshift не установлен. Снапшот не создан."
warn "Установи: sudo apt install timeshift — затем настрой и создай снапшот вручную"
fi
}
# ─── Объяснение ошибок apt ───────────────────────────────────────────────────
explain_apt_error() {
local rc="$1"
local log_tail
log_tail=$(tail -40 "$LOG_FILE" 2>/dev/null || true)
error "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
error " APT/dpkg завершился с кодом ошибки: $rc"
error "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
if echo "$log_tail" | grep -qE "lock-frontend|Could not open lock|unable to acquire"; then
error ""
error "🔒 ПРИЧИНА: Файл блокировки dpkg/apt занят другим процессом"
error ""
error " Решение:"
error " 1. Подожди 1-2 мин — возможно, идёт фоновое auto-update"
error " 2. Найди блокирующий процесс:"
error " sudo lsof /var/lib/dpkg/lock-frontend"
error " 3. Убей если завис: sudo kill -9 <PID>"
error " 4. Если процесса нет — удали lock-файлы:"
error " sudo rm -f /var/lib/apt/lists/lock"
error " sudo rm -f /var/cache/apt/archives/lock"
error " sudo rm -f /var/lib/dpkg/lock /var/lib/dpkg/lock-frontend"
error " sudo dpkg --configure -a"
error " 5. Запусти скрипт снова"
return
fi
if echo "$log_tail" | grep -qE "broken packages|fix-broken|unmet dep|dependency prob"; then
error ""
error "💥 ПРИЧИНА: Сломанные или неудовлетворённые зависимости"
error ""
error " Решение:"
error " sudo dpkg --configure -a"
error " sudo apt --fix-broken install"
error " sudo apt update && sudo apt dist-upgrade"
error " Затем запусти скрипт снова"
return
fi
if echo "$log_tail" | grep -q "kept back"; then
error ""
error "📌 ПРИЧИНА: Пакеты удерживаются (held back) и не обновляются"
error ""
error " Решение:"
error " sudo apt full-upgrade"
error " # или:"
error " sudo apt-mark showhold"
error " sudo apt-mark unhold <пакет>"
return
fi
if echo "$log_tail" | grep -qE "No space left|ENOSPC"; then
error ""
error "💾 ПРИЧИНА: Закончилось место на диске"
error ""
error " Решение:"
error " df -h # где мало места"
error " sudo apt autoremove --purge # удалить ненужные пакеты"
error " sudo apt clean # очистить кэш apt"
error " sudo journalctl --vacuum-size=200M # очистить systemd-логи"
error " sudo find /var/log -name '*.gz' -delete # старые логи"
return
fi
if echo "$log_tail" | grep -qE "^Err:|404 Not Found|Failed to fetch|Unable to fetch"; then
error ""
error "🌐 ПРИЧИНА: Репозиторий недоступен или URL устарел (возможно — старый PPA)"
error ""
error " Решение:"
error " sudo apt update 2>&1 | grep '^Err:'"
error " ls /etc/apt/sources.list.d/ # найди проблемный файл"
error " sudo mv /etc/apt/sources.list.d/<проблема>.list /tmp/"
error " sudo apt update"
error " Затем запусти скрипт снова"
return
fi
if echo "$log_tail" | grep -qE "dpkg.*error|subprocess.*returned error|post-installation"; then
error ""
error "⚙️ ПРИЧИНА: Ошибка при конфигурации или установке пакета (dpkg)"
error ""
error " Решение:"
error " sudo dpkg --configure -a"
error " sudo apt --fix-broken install"
error " # Принудительная конфигурация:"
error " sudo dpkg --force-configure-any --configure -a"
error " # Крайний случай — удалить сломанный пакет:"
error " sudo dpkg --remove --force-remove-reinstreq <пакет>"
return
fi
error ""
error "❓ Автоматическая диагностика не определила причину."
error ""
error " Что делать:"
error " 1. Посмотри хвост лога: tail -50 $LOG_FILE"
error " 2. Выполни:"
error " sudo dpkg --configure -a"
error " sudo apt --fix-broken install"
error " sudo apt update"
error " 3. Запусти скрипт снова"
}
explain_do_release_error() {
local log_tail
log_tail=$(tail -60 "$LOG_FILE" 2>/dev/null || true)
error "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
error " do-release-upgrade завершился с ошибкой"
error "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
if echo "$log_tail" | grep -q "No new release found"; then
error ""
error "🔍 ПРИЧИНА: Новый релиз не найден"
error ""
error " Решение:"
error " cat /etc/update-manager/release-upgrades # проверь Prompt=lts"
error " sudo sed -i 's/Prompt=.*/Prompt=lts/' /etc/update-manager/release-upgrades"
error " sudo do-release-upgrade # повторить"
error " Или принудительно:"
error " sudo do-release-upgrade -d"
return
fi
if echo "$log_tail" | grep -qE "unresolvable|could not calculate|dependency|conflicts"; then
error ""
error "💥 ПРИЧИНА: Конфликт зависимостей при расчёте апгрейда"
error " Обычно из-за сторонних PPA или нестандартных пакетов"
error ""
error " Решение:"
error " 1. Детальный лог: cat /var/log/dist-upgrade/apt.log | tail -80"
error " 2. Убедись, что PPA отключены (скрипт делает это автоматически)"
error " 3. Если мешает конкретный пакет:"
error " sudo apt-get remove <проблемный-пакет>"
error " 4. Запусти скрипт снова"
return
fi
if echo "$log_tail" | grep -q "held back"; then
error ""
error "📌 ПРИЧИНА: Удержанные пакеты блокируют апгрейд"
error ""
error " Решение:"
error " sudo apt-mark showhold"
error " sudo apt-mark unhold <пакет>"
error " sudo apt full-upgrade"
error " Затем запусти скрипт снова"
return
fi
if echo "$log_tail" | grep -qE "Authentication|GPG|NO_PUBKEY"; then
error ""
error "🔑 ПРИЧИНА: Ошибка GPG-ключа репозитория"
error ""
error " Решение:"
error " sudo apt-key list # посмотреть ключи (deprecated)"
error " sudo apt update 2>&1 | grep 'NO_PUBKEY'"
error " # Добавить ключ:"
error " sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys <KEY_ID>"
return
fi
error ""
error "❓ Причина не определена автоматически. Смотри логи:"
error " Основной лог: $LOG_FILE"
error " Лог апгрейда: /var/log/dist-upgrade/apt.log"
error " Детальный лог: /var/log/dist-upgrade/main.log"
error ""
error " Попробуй запустить вручную для интерактивного режима:"
error " sudo do-release-upgrade"
}
# ─── Безопасный запуск apt ───────────────────────────────────────────────────
run_apt() {
local desc="$1"; shift
info "▶ $desc"
local rc=0
DEBIAN_FRONTEND=noninteractive "$@" \
-o Dpkg::Options::="--force-confdef" \
-o Dpkg::Options::="--force-confold" \
2>&1 | tee -a "$LOG_FILE" || rc=$?
if (( rc != 0 )); then
explain_apt_error "$rc"
die "$desc завершился с ошибкой (код $rc)"
fi
ok "$desc — готово"
}
# ─── Обновить текущую систему ─────────────────────────────────────────────────
update_current_system() {
step "Обновление текущей системы"
run_apt "apt-get update" apt-get update
run_apt "apt-get upgrade" apt-get upgrade -y
run_apt "apt-get dist-upgrade" apt-get dist-upgrade -y
run_apt "apt-get autoremove" apt-get autoremove -y --purge
apt-get clean 2>&1 | tee -a "$LOG_FILE" || true
ok "Система полностью обновлена"
}
# ─── do-release-upgrade ───────────────────────────────────────────────────────
do_upgrade() {
local target="$1"
step "Запуск do-release-upgrade → Ubuntu $target"
apt-get install -y update-manager-core 2>&1 | tee -a "$LOG_FILE" || true
# Установить Prompt=lts
if [[ -f /etc/update-manager/release-upgrades ]]; then
sed -i 's/Prompt=.*/Prompt=lts/' /etc/update-manager/release-upgrades
fi
local rc=0
DEBIAN_FRONTEND=noninteractive do-release-upgrade \
-f DistUpgradeViewNonInteractive \
2>&1 | tee -a "$LOG_FILE" || rc=$?
if (( rc != 0 )); then
explain_do_release_error
die "do-release-upgrade → Ubuntu $target завершился с ошибкой (код $rc)"
fi
ok "do-release-upgrade → Ubuntu $target — завершён успешно"
}
# ─── Перезагрузка с сохранением состояния ────────────────────────────────────
do_reboot() {
local next_state="$1"
set_state "$next_state"
echo ""
step "Перезагрузка системы"
info "После перезагрузки запусти скрипт снова:"
echo -e " ${BLD}sudo bash $SCRIPT_PATH${RST}"
echo ""
info "Перезагрузка через 10 секунд... (Ctrl+C — отменить)"
sleep 10
reboot
}
# ─── MAIN ─────────────────────────────────────────────────────────────────────
main() {
check_root
init_dirs
local ver
ver=$(get_ubuntu_version)
local state
state=$(get_state)
echo -e "\n${BLD}${CYN}╔══════════════════════════════════════════════╗${RST}"
echo -e "${BLD}${CYN}║ Ubuntu Upgrade Helper 20.04 → 24.04 ║${RST}"
echo -e "${BLD}${CYN}╚══════════════════════════════════════════════╝${RST}\n"
info "Версия: Ubuntu $ver | Состояние: $state"
info "Лог: $LOG_FILE"
echo ""
case "$ver" in
"20.04")
step "Этап 1/2: Ubuntu 20.04 → 22.04 LTS (Jammy Jellyfish)"
# Защита от повторного входа после неудачного апгрейда
if [[ "$state" == "after_upgrade_20_22" ]]; then
error "Состояние указывает, что апгрейд до 22.04 был запущен,"
error "но мы всё ещё на 20.04."
error ""
error "Возможные причины:"
error " - Апгрейд прервался (прочитай: /var/log/dist-upgrade/main.log)"
error " - do-release-upgrade завершился, но не перезагрузился"
error ""
error "Попробуй вручную: sudo do-release-upgrade"
die "Неожиданное состояние. Проверь логи."
fi
preflight_checks "22.04"
take_snapshot "pre-upgrade-20-to-22"
disable_ppas
update_current_system
do_upgrade "22.04"
do_reboot "after_upgrade_20_22"
;;
"22.04")
step "Этап 2/2: Ubuntu 22.04 → 24.04 LTS (Noble Numbat)"
preflight_checks "24.04"
take_snapshot "pre-upgrade-22-to-24"
disable_ppas
update_current_system
do_upgrade "24.04"
do_reboot "after_upgrade_22_24"
;;
"24.04")
step "Ubuntu 24.04 LTS — цель достигнута!"
restore_ppas
set_state "complete"
update_current_system
echo ""
echo -e "${BLD}${GRN}╔══════════════════════════════════════════════╗${RST}"
echo -e "${BLD}${GRN}║ ✅ Апгрейд успешно завершён! ║${RST}"
echo -e "${BLD}${GRN}║ Ubuntu $(lsb_release -rs) $(lsb_release -cs) ║${RST}"
echo -e "${BLD}${GRN}╚══════════════════════════════════════════════╝${RST}"
echo ""
info "Что проверить после апгрейда:"
info " 1. Сторонние PPA — проверь совместимость:"
info " ls /etc/apt/sources.list.d/"
info " 2. Драйверы: ubuntu-drivers devices"
info " 3. Работу сети, принтеров, VPN"
info " 4. Полный лог: $LOG_FILE"
;;
*)
die "Неожиданная версия Ubuntu: $ver. Скрипт поддерживает 20.04, 22.04 и 24.04."
;;
esac
}
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment