Handle authorized Debian upgrades and verified BIOS GPT repair
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
# 原生 Shell 仓库
|
||||
|
||||
- 直接在本仓库维护原生 Bash 源码;目标机不需要 Python 载荷或私有仓库。
|
||||
- install.sh 是 Debian 13 的最小 Komodo 接入;独立 upgrade-debian12-13.sh 由人执行。
|
||||
- install.sh 是 Debian 13 的最小 Komodo 接入;独立 upgrade-debian12-13.sh 默认交互执行,已有升级与恢复授权可用 --yes --recovery-confirmed;受限 BIOS/GPT 修复需显式 --repair-bios-boot。
|
||||
- 支持 curl -fsSL URL | bash -s -- 参数;管道模式不能依赖脚本所在目录。交互升级读取 /dev/tty。
|
||||
- 数据参数按字符串处理,不 eval 或 source 用户配置;不把 API/DNS token、私钥写入公开文件。
|
||||
- 重跑保留身份。信任、公钥、现有配置冲突先停止;不重置网络、防火墙或已有数据库。
|
||||
|
||||
@@ -69,7 +69,9 @@ bash upgrade-debian12-13.sh --verify
|
||||
```
|
||||
|
||||
resume/verify 可使用同一固定版本 URL 再次下载执行,或保存该版本的文件后执行。
|
||||
升级使用真实终端 /dev/tty,APT 配置冲突由人判断,不自动重启、不自动 autoremove。
|
||||
默认升级使用真实终端 /dev/tty,APT 配置冲突由操作者判断。已经授权维护并确认救援入口时,可显式传 `--yes --recovery-confirmed`,使用非交互 APT 并保留本地配置;仍不自动重启、不自动 autoremove。升级和重启后的 resume 分别执行,不能并发运行。
|
||||
|
||||
官方 `deb-src` 和 Deb822 `Types: deb deb-src` 现在随二进制源一起校验和转换,不必临时禁用。BIOS/GPT 缺少 BIOS Boot 分区会在 check 阶段报错;已授权磁盘引导修复可给 upgrade/resume 增加 `--repair-bios-boot`。只自动处理 512 字节扇区、单个 Linux 分区从 2048 扇区开始、GPT first-lba 为 34 的已验证布局,在空闲 34–2047 扇区新增 BIOS Boot 分区。修改前备份 GPT 和磁盘首尾,修改后逐字校验原分区条目并重新安装 GRUB;其他布局停止,不移动或缩小已有分区。该选项不替代提供商救援入口和异机备份。
|
||||
仅接受显式官方 Debian 源的受支持格式;第三方源、混合版本、复杂 Deb822 需先人工处理。
|
||||
检查 APT、磁盘、UEFI 挂载;实际引导布局、救援入口和应用备份由操作者在升级前确认。
|
||||
状态与系统归档在 /var/lib/platform-debian-upgrade;归档不替代异机或应用一致性备份。
|
||||
|
||||
@@ -2,8 +2,76 @@
|
||||
# Independent manual upgrade. Never invoked by install.sh.
|
||||
set -euo pipefail
|
||||
export LC_ALL=C
|
||||
usage() { echo 'Usage: bash upgrade-debian12-13.sh --check|--upgrade|--resume|--verify'; }
|
||||
usage() { echo 'Usage: bash upgrade-debian12-13.sh --check|--upgrade|--resume|--verify [--yes --recovery-confirmed] [--repair-bios-boot]'; }
|
||||
fail() { printf 'ERROR: %s\n' "$*" >&2; return 1; }
|
||||
# Only the verified single-partition, 512-byte-sector GPT layout is repairable.
|
||||
bios_gap_plan() {
|
||||
awk '
|
||||
/^label: / {if($2!="gpt")bad=1; label++}
|
||||
/^sector-size: / {if($2!=512)bad=1; sector++}
|
||||
/^first-lba: / {if($2!=34)bad=1; first++}
|
||||
/^\/dev\// {
|
||||
count++; line=$0
|
||||
if(line !~ /start=[ ]*2048,/ || line !~ /type=0FC63DAF-8483-4772-8E79-3D69D8477DE4/)bad=1
|
||||
}
|
||||
END {if(bad || label!=1 || sector!=1 || first!=1 || count!=1)exit 1; print "34 2014"}' "$1"
|
||||
}
|
||||
check_bios_boot() {
|
||||
[[ ! -d /sys/firmware/efi ]] || return 0
|
||||
local root_device parent disk dump before after sectors
|
||||
root_device=$(grub-probe --target=device /boot) || return 1
|
||||
[[ "$root_device" =~ ^/dev/(vd[a-z]|sd[a-z]|nvme[0-9]+n[0-9]+p)[0-9]+$ ]] || { fail 'Review unsupported boot device layout'; return 1; }
|
||||
parent=$(lsblk -ndo PKNAME "$root_device")
|
||||
[[ "$parent" =~ ^(vd[a-z]|sd[a-z]|nvme[0-9]+n[0-9]+)$ ]] || { fail 'A single physical boot disk is required'; return 1; }
|
||||
disk=/dev/$parent
|
||||
dump=$(sfdisk --dump "$disk") || return 1
|
||||
[[ "$dump" == *'label: gpt'* ]] || return 0
|
||||
if grep -qi 'type=21686148-6449-6E6F-744E-656564454649' <<< "$dump"; then
|
||||
if [[ -f "$state/bios-gpt-before.sfdisk" && ! -f "$state/bios-gpt-after.sfdisk" && "$action" != --check ]]; then
|
||||
cmp -s <(grep '^/dev/' "$state/bios-gpt-before.sfdisk") <(grep '^/dev/' <<< "$dump" | grep -vi 'type=21686148-6449-6E6F-744E-656564454649') || { fail 'Original partition entry changed during interrupted repair'; return 1; }
|
||||
grub-install --target=i386-pc "$disk"
|
||||
update-grub
|
||||
printf '%s\n' "$dump" > "$state/bios-gpt-after.sfdisk"
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
if [[ "$action" == --check || "$repair_bios" != true ]]; then
|
||||
fail 'BIOS/GPT has no BIOS Boot partition; review --repair-bios-boot with recovery access'; return 1
|
||||
fi
|
||||
[[ -s "$state/before.tar" ]] || { fail 'Original upgrade backup required'; return 1; }
|
||||
before=$state/bios-gpt-before.sfdisk
|
||||
if [[ -e "$before" ]]; then
|
||||
cmp -s "$before" <(printf '%s\n' "$dump") || { fail 'Boot layout changed after backup'; return 1; }
|
||||
else printf '%s\n' "$dump" > "$before"; fi
|
||||
bios_gap_plan "$before" >/dev/null || { fail 'No supported empty BIOS boot gap; no partition changes'; return 1; }
|
||||
sectors=$(blockdev --getsz "$disk")
|
||||
[[ "$sectors" =~ ^[0-9]+$ && "$sectors" -gt 4096 ]] || return 1
|
||||
[[ -e "$state/bios-first-1m.bin" ]] || dd if="$disk" of="$state/bios-first-1m.bin" bs=512 count=2048 status=none
|
||||
[[ -e "$state/bios-last-33.bin" ]] || dd if="$disk" of="$state/bios-last-33.bin" bs=512 skip=$((sectors-33)) count=33 status=none
|
||||
[[ $(stat -c %s "$state/bios-first-1m.bin") == 1048576 && $(stat -c %s "$state/bios-last-33.bin") == 16896 ]] || return 1
|
||||
printf 'start=34, size=2014, type=21686148-6449-6E6F-744E-656564454649, name="BIOS boot"\n' |
|
||||
sfdisk --append --no-reread --no-tell-kernel --wipe never --wipe-partitions never "$disk"
|
||||
sfdisk --verify "$disk"
|
||||
after=$(sfdisk --dump "$disk")
|
||||
# Preserve every byte of the original partition entry (UUID, offset and size).
|
||||
cmp -s <(grep '^/dev/' "$before") <(grep '^/dev/' <<< "$after" | grep -vi 'type=21686148-6449-6E6F-744E-656564454649') || { fail 'Original partition entry changed; inspect saved GPT before proceeding'; return 1; }
|
||||
partx -a "$disk" || partx -u "$disk"
|
||||
grub-install --target=i386-pc "$disk"
|
||||
update-grub
|
||||
printf '%s\n' "$after" > "$state/bios-gpt-after.sfdisk"
|
||||
}
|
||||
confirm_upgrade() {
|
||||
local expected=$1 prompt=$2 answer
|
||||
if "$assume_yes"; then return 0; fi
|
||||
printf '%s Type %s: ' "$prompt" "$expected" >/dev/tty
|
||||
read -r answer </dev/tty
|
||||
[[ "$answer" == "$expected" ]]
|
||||
}
|
||||
apt_upgrade() {
|
||||
if "$assume_yes"; then
|
||||
DEBIAN_FRONTEND=noninteractive APT_LISTCHANGES_FRONTEND=none apt-get -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold "$@" </dev/null
|
||||
else apt-get "$@" </dev/tty; fi
|
||||
}
|
||||
# Validate only explicitly supported official source syntax. Unknown layouts stop.
|
||||
validate_source() {
|
||||
local file=$1 suite=$2
|
||||
@@ -16,7 +84,8 @@ validate_source() {
|
||||
if(!have)return
|
||||
if(enabled!="" && enabled!="yes" && enabled!="no")reject()
|
||||
if(enabled=="no"){reset();return}
|
||||
if(types!="deb" || !valid_uri(uri) || suites=="" || components=="")reject()
|
||||
if(types!="deb" && types!="deb-src" && types!="deb deb-src" && types!="deb-src deb")reject()
|
||||
if(!valid_uri(uri) || suites=="" || components=="")reject()
|
||||
n=split(suites,a,/[ \t]+/);for(i=1;i<=n;i++)if(!valid_suite(a[i]))reject()
|
||||
if(uri ~ /debian-security\/?$/ && suites!=suite "-security")reject()
|
||||
if(uri !~ /debian-security\/?$/ && suites ~ /-security/)reject()
|
||||
@@ -26,8 +95,8 @@ validate_source() {
|
||||
/^[ \t]*$/ {if(deb822)block();next}
|
||||
!deb822 {
|
||||
line=$0;sub(/[ \t]+#.*/,"",line)
|
||||
if(line !~ /^[ \t]*deb[ \t]+/) {reject();next}
|
||||
sub(/^[ \t]*deb[ \t]+/,"",line)
|
||||
if(line !~ /^[ \t]*deb(-src)?[ \t]+/) {reject();next}
|
||||
sub(/^[ \t]*deb(-src)?[ \t]+/,"",line)
|
||||
if(line ~ /^\[/){
|
||||
if(line !~ /^\[[^]]+\][ \t]+/){reject();next}
|
||||
options=line;sub(/\].*$/,"",options);sub(/^\[/,"",options)
|
||||
@@ -74,7 +143,7 @@ render_source() {
|
||||
}
|
||||
!deb822 {
|
||||
line=$0
|
||||
if(match(line,/^[ \t]*deb[ \t]+(\[[^]]+\][ \t]+)?[^ \t]+[ \t]+/)){
|
||||
if(match(line,/^[ \t]*deb(-src)?[ \t]+(\[[^]]+\][ \t]+)?[^ \t]+[ \t]+/)){
|
||||
prefix=substr(line,1,RLENGTH);tail=substr(line,RLENGTH+1)
|
||||
sub(/^bookworm/,"trixie",tail);line=prefix tail
|
||||
}
|
||||
@@ -146,7 +215,14 @@ save_phase() {
|
||||
main() {
|
||||
local action=${1:---help}
|
||||
case "$action" in --help|-h) usage; return 0;; --check|--upgrade|--resume|--verify) :;; *) usage; return 1;; esac
|
||||
[[ $# == 1 && $EUID == 0 && -d /run/systemd/system && ! -e /.dockerenv ]] || { fail 'Run on a native systemd host as root'; return 1; }
|
||||
shift
|
||||
local assume_yes=false recovery_confirmed=false repair_bios=false
|
||||
while (($#)); do
|
||||
case "$1" in --yes) assume_yes=true;; --recovery-confirmed) recovery_confirmed=true;; --repair-bios-boot) repair_bios=true;; *) fail "Unknown argument: $1"; return 1;; esac
|
||||
shift
|
||||
done
|
||||
! "$assume_yes" || "$recovery_confirmed" || { fail '--yes requires --recovery-confirmed'; return 1; }
|
||||
[[ $EUID == 0 && -d /run/systemd/system && ! -e /.dockerenv ]] || { fail 'Run on a native systemd host as root'; return 1; }
|
||||
grep -Eq '^ID="?debian"?$' /etc/os-release || { fail 'Debian required'; return 1; }
|
||||
local state=/var/lib/platform-debian-upgrade phase='' boot_id answer file
|
||||
boot_id=$(cat /proc/sys/kernel/random/boot_id)
|
||||
@@ -159,7 +235,7 @@ main() {
|
||||
package_checks
|
||||
if [[ "$action" != --check ]]; then
|
||||
[[ "$action" == --upgrade || -d "$state" ]] || { fail 'No original upgrade state'; return 1; }
|
||||
[[ -r /dev/tty && -w /dev/tty ]] || { fail 'A real controlling terminal is required'; return 1; }
|
||||
"$assume_yes" || [[ -r /dev/tty && -w /dev/tty ]] || { fail 'A real controlling terminal is required'; return 1; }
|
||||
umask 077
|
||||
install -d -m 0700 "$state"
|
||||
[[ ! -L "$state/lock" && ( ! -e "$state/lock" || ( -f "$state/lock" && $(stat -c %h "$state/lock") == 1 ) ) ]] || { fail 'Invalid lock'; return 1; }
|
||||
@@ -182,13 +258,12 @@ main() {
|
||||
scan_sources bookworm
|
||||
[[ $(df -Pk / | awk 'NR==2{print $4}') -ge 5242880 && $(df -Pk /boot | awk 'NR==2{print $4}') -ge 262144 ]] || { fail 'Need 5 GiB free on / and 256 MiB on /boot'; return 1; }
|
||||
if [[ -d /sys/firmware/efi ]]; then mountpoint -q /boot/efi || { fail 'Mount and review the EFI System Partition first'; return 1; }; fi
|
||||
if [[ "$action" == --check ]]; then echo 'Source/package/storage checks passed; review bootloader and recovery access before --upgrade.'; return 0; fi
|
||||
if [[ "$action" == --check ]]; then check_bios_boot; echo 'Source/package/storage/boot-layout checks passed; review recovery access before --upgrade.'; return 0; fi
|
||||
fi
|
||||
[[ -r /dev/tty && -w /dev/tty ]] || { fail 'A real controlling terminal is required'; return 1; }
|
||||
"$assume_yes" || [[ -r /dev/tty && -w /dev/tty ]] || { fail 'A real controlling terminal is required'; return 1; }
|
||||
[[ ( "$action" == --upgrade && -z "$phase" ) || ( "$action" == --resume && -n "$phase" ) ]] || { fail 'Use --upgrade for a new run or --resume for the existing run'; return 1; }
|
||||
if [[ -z "$phase" ]]; then
|
||||
printf 'Bootloader, rescue access and required backups reviewed. Type UPGRADE: ' >/dev/tty
|
||||
read -r answer </dev/tty; [[ "$answer" == UPGRADE ]] || return 1
|
||||
confirm_upgrade UPGRADE 'Bootloader, rescue access and required backups reviewed.' || return 1
|
||||
tar --acls --xattrs -cpf "$state/before.tar" /etc /boot /var/lib/dpkg/status
|
||||
tar -tf "$state/before.tar" >/dev/null
|
||||
: > "$state/identities.sha256"
|
||||
@@ -197,9 +272,10 @@ main() {
|
||||
fi
|
||||
case "$phase" in prepared|bookworm_updated|switching_sources|sources_switched|awaiting_reboot|verified) :;; *) fail 'Unknown recorded phase'; return 1;; esac
|
||||
if [[ "$phase" == awaiting_reboot || "$phase" == verified ]]; then echo 'Upgrade already finished; reboot manually and use --verify.'; return 0; fi
|
||||
check_bios_boot
|
||||
if [[ "$phase" == prepared ]]; then
|
||||
scan_sources bookworm
|
||||
apt-get update; apt-get full-upgrade </dev/tty
|
||||
apt-get update; apt_upgrade full-upgrade
|
||||
package_checks
|
||||
printf '%s\n' "$boot_id" > "$state/bookworm-boot"
|
||||
phase=bookworm_updated; save_phase "$phase"
|
||||
@@ -209,8 +285,7 @@ main() {
|
||||
! reboot_pending || { fail 'Reboot before continuing'; return 1; }
|
||||
[[ ! -e "$state/reboot-required" || "$boot_id" != "$(cat "$state/bookworm-boot")" ]] || return 1
|
||||
scan_sources bookworm
|
||||
printf 'Switch the reviewed Debian sources to trixie. Type TRIXIE: ' >/dev/tty
|
||||
read -r answer </dev/tty; [[ "$answer" == TRIXIE ]] || return 1
|
||||
confirm_upgrade TRIXIE 'Switch the reviewed Debian sources to trixie.' || return 1
|
||||
uname -r > "$state/bookworm-kernel"
|
||||
install -d -m 0700 "$state/sources"
|
||||
printf '%s\n' "${source_files[@]}" > "$state/source-files"
|
||||
@@ -228,8 +303,8 @@ main() {
|
||||
scan_sources trixie
|
||||
while IFS= read -r file; do cmp -s "$file" "$state/sources$file.target" || { fail 'Target source changed'; return 1; }; done < "$state/source-files"
|
||||
apt-get update
|
||||
apt-get upgrade --without-new-pkgs </dev/tty
|
||||
apt-get full-upgrade </dev/tty
|
||||
apt_upgrade upgrade --without-new-pkgs
|
||||
apt_upgrade full-upgrade
|
||||
package_checks
|
||||
printf '%s\n' "$boot_id" > "$state/upgrade-boot"
|
||||
save_phase awaiting_reboot
|
||||
|
||||
Reference in New Issue
Block a user