Skip to content

Commit 78a5f3d

Browse files
authored
Merge pull request #20 from GlitchSlayed/codex/add-rollback-infrastructure-with-manifests
Add manifest-based rollback workflow and generate `/usr/local/sbin/uki-rollback.sh`
2 parents 030fe82 + f373140 commit 78a5f3d

1 file changed

Lines changed: 278 additions & 3 deletions

File tree

‎uki-setup.sh‎

Lines changed: 278 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,11 @@ SELF="$(realpath "$0")"
7070
BUILD_SCRIPT="/usr/local/sbin/uki-build.sh"
7171
INSTALL_PLUGIN="/usr/lib/kernel/install.d/90-uki-ukify.install"
7272
BACKUP_ROOT="/var/backups/uki-setup"
73+
RUN_TIMESTAMP="$(date -u +%Y%m%dT%H%M%SZ)"
74+
RUN_BACKUP_DIR="${BACKUP_ROOT}/${RUN_TIMESTAMP}"
75+
RUN_MANIFEST_DIR="${BACKUP_ROOT}/manifests"
76+
RUN_MANIFEST_PATH="${RUN_MANIFEST_DIR}/${RUN_TIMESTAMP}.manifest"
77+
ROLLBACK_SCRIPT="/usr/local/sbin/uki-rollback.sh"
7378
PKG_MGR=""
7479
PKG_INSTALL_CMD=()
7580
DRY_RUN=0
@@ -83,6 +88,46 @@ die() { echo "${RED}${BLD}[err]${RST} $*" >&2; exit 1; }
8388
hr() { echo "──────────────────────────────────────────────────────────────"; }
8489
require_cmd() { command -v "$1" &>/dev/null || die "Required command missing: $1"; }
8590

91+
refresh_run_paths() {
92+
RUN_BACKUP_DIR="${BACKUP_ROOT}/${RUN_TIMESTAMP}"
93+
RUN_MANIFEST_DIR="${BACKUP_ROOT}/manifests"
94+
RUN_MANIFEST_PATH="${RUN_MANIFEST_DIR}/${RUN_TIMESTAMP}.manifest"
95+
}
96+
97+
init_run_manifest() {
98+
refresh_run_paths
99+
mkdir -p "$RUN_MANIFEST_DIR" "$RUN_BACKUP_DIR"
100+
cat > "$RUN_MANIFEST_PATH" <<EOF
101+
# uki-setup manifest v1
102+
timestamp=${RUN_TIMESTAMP}
103+
script=uki-setup.sh
104+
backup_dir=${RUN_BACKUP_DIR}
105+
EOF
106+
}
107+
108+
ensure_manifest_ready() {
109+
refresh_run_paths
110+
if [[ ! -f "$RUN_MANIFEST_PATH" ]]; then
111+
init_run_manifest
112+
fi
113+
}
114+
115+
append_manifest_entry() {
116+
local op="$1" path="$2" backup_ref="${3:-}" extra="${4:-}"
117+
ensure_manifest_ready
118+
printf '%s|%s|%s|%s\n' "$op" "$path" "$backup_ref" "$extra" >> "$RUN_MANIFEST_PATH"
119+
}
120+
121+
record_created_path() {
122+
local path="$1"
123+
append_manifest_entry "created" "$path" ""
124+
}
125+
126+
record_deleted_path() {
127+
local path="$1" backup_ref="${2:-}"
128+
append_manifest_entry "deleted" "$path" "$backup_ref"
129+
}
130+
86131
list_installed_kernels() {
87132
local pkg kernel_ver
88133

@@ -442,12 +487,13 @@ ensure_esp_mounted() {
442487
backup_path() {
443488
local src="$1"
444489
[[ -e "$src" || -L "$src" ]] || return 0
445-
local ts backup_dir backup
446-
ts="$(date +%Y%m%d-%H%M%S)"
447-
backup_dir="${BACKUP_ROOT}/${ts}"
490+
local backup_dir backup
491+
refresh_run_paths
492+
backup_dir="${RUN_BACKUP_DIR}"
448493
backup="${backup_dir}${src}"
449494
mkdir -p "$(dirname "$backup")"
450495
cp -a "$src" "$backup"
496+
append_manifest_entry "modified" "$src" "$backup"
451497
info "Backed up ${src} -> ${backup}"
452498
}
453499

@@ -607,6 +653,10 @@ BOOT_SUCCESS_DIR="/var/lib/uki-ukify/boot-success"
607653
BACKUP_ROOT="/var/backups/uki-setup"
608654
EFI_AUDIT_LOG="/var/log/uki-efi-audit.log"
609655
DRY_RUN=0
656+
RUN_TIMESTAMP="$(date -u +%Y%m%dT%H%M%SZ)"
657+
RUN_BACKUP_DIR="${BACKUP_ROOT}/${RUN_TIMESTAMP}"
658+
RUN_MANIFEST_DIR="${BACKUP_ROOT}/manifests"
659+
RUN_MANIFEST_PATH="${RUN_MANIFEST_DIR}/${RUN_TIMESTAMP}.manifest"
610660
# ─────────────────────────────────────────────────────────────────────────────
611661
612662
RED='\e[31;1m'; GRN='\e[32;1m'; YLW='\e[33;1m'; RST='\e[0m'
@@ -615,6 +665,46 @@ warn() { echo -e "${YLW}[uki-build]${RST} $*" >&2; }
615665
die() { echo -e "${RED}[uki-build]${RST} $*" >&2; exit 1; }
616666
require_cmd() { command -v "$1" &>/dev/null || die "Required command missing: $1"; }
617667
668+
refresh_run_paths() {
669+
RUN_BACKUP_DIR="${BACKUP_ROOT}/${RUN_TIMESTAMP}"
670+
RUN_MANIFEST_DIR="${BACKUP_ROOT}/manifests"
671+
RUN_MANIFEST_PATH="${RUN_MANIFEST_DIR}/${RUN_TIMESTAMP}.manifest"
672+
}
673+
674+
init_run_manifest() {
675+
refresh_run_paths
676+
mkdir -p "$RUN_MANIFEST_DIR" "$RUN_BACKUP_DIR"
677+
cat > "$RUN_MANIFEST_PATH" <<EOF
678+
# uki-setup manifest v1
679+
timestamp=${RUN_TIMESTAMP}
680+
script=uki-build.sh
681+
backup_dir=${RUN_BACKUP_DIR}
682+
EOF
683+
}
684+
685+
ensure_manifest_ready() {
686+
refresh_run_paths
687+
if [[ ! -f "$RUN_MANIFEST_PATH" ]]; then
688+
init_run_manifest
689+
fi
690+
}
691+
692+
append_manifest_entry() {
693+
local op="$1" path="$2" backup_ref="${3:-}" extra="${4:-}"
694+
ensure_manifest_ready
695+
printf '%s|%s|%s|%s\n' "$op" "$path" "$backup_ref" "$extra" >> "$RUN_MANIFEST_PATH"
696+
}
697+
698+
backup_path_for_manifest() {
699+
refresh_run_paths
700+
local src="$1"
701+
[[ -e "$src" || -L "$src" ]] || return 1
702+
local backup="${RUN_BACKUP_DIR}${src}"
703+
mkdir -p "$(dirname "$backup")"
704+
cp -a "$src" "$backup"
705+
echo "$backup"
706+
}
707+
618708
list_installed_kernels() {
619709
local pkg kernel_ver
620710
@@ -662,6 +752,7 @@ backup_efibootmgr_snapshot() {
662752
fi
663753
664754
efibootmgr -v > "$snapshot_file"
755+
append_manifest_entry "efi_snapshot" "efibootmgr" "$snapshot_file" "$reason"
665756
info "Saved efibootmgr snapshot: ${snapshot_file}"
666757
audit_efi_event "Saved efibootmgr snapshot ${snapshot_file}"
667758
}
@@ -714,6 +805,10 @@ delete_uki_and_entry() {
714805
715806
uki="${EFI_DIR}/linux-${kernel_ver}.efi"
716807
if [[ -f "$uki" ]]; then
808+
local deleted_backup=""
809+
if deleted_backup=$(backup_path_for_manifest "$uki"); then
810+
append_manifest_entry "deleted" "$uki" "$deleted_backup"
811+
fi
717812
log_or_run "Remove stale UKI ${uki}" rm -f "$uki"
718813
audit_efi_event "Removed stale UKI ${uki}"
719814
fi
@@ -1221,9 +1316,15 @@ ESP_MOUNT_CHECK=$(find_mounted_esp_target || true)
12211316
[[ -n "$ESP_MOUNT_CHECK" ]] || die "ESP mount verification failed after mount attempts. Checked candidates: ${ESP_MOUNT_CANDIDATES[*]}. Fix: mount your ESP manually (typically /boot/efi) and re-run."
12221317
validate_esp_free_space "$ESP_MOUNT_CHECK"
12231318
1319+
init_run_manifest
12241320
EFFECTIVE_CMDLINE=$(get_effective_cmdline)
12251321
12261322
cleanup_items=("$INITRD_OUT")
1323+
if [[ -e "$UKI_OUT" || -L "$UKI_OUT" ]]; then
1324+
if prev_backup=$(backup_path_for_manifest "$UKI_OUT"); then
1325+
append_manifest_entry "modified" "$UKI_OUT" "$prev_backup"
1326+
fi
1327+
fi
12271328
12281329
info "Stage 1/2: Building standalone initramfs via dracut: ${INITRD_OUT}"
12291330
if [[ "$DRY_RUN" -eq 1 ]]; then
@@ -1275,6 +1376,9 @@ if [[ "$DRY_RUN" -eq 1 ]]; then
12751376
info "[dry-run] Would run: ukify ${UKIFY_ARGS[*]}"
12761377
else
12771378
ukify "${UKIFY_ARGS[@]}"
1379+
if [[ -f "$UKI_OUT" ]]; then
1380+
append_manifest_entry "created" "$UKI_OUT" "" "kernel=${KERNEL_VER}"
1381+
fi
12781382
fi
12791383
12801384
extract_uki_section_text() {
@@ -1420,9 +1524,175 @@ BUILDBODY
14201524
"$BUILD_SCRIPT"
14211525

14221526
chmod 0755 "$BUILD_SCRIPT"
1527+
record_created_path "$BUILD_SCRIPT"
14231528
info "Build script written."
14241529
}
14251530

1531+
1532+
phase_write_rollback_script() {
1533+
hr
1534+
info "Phase 4: Writing rollback script → ${ROLLBACK_SCRIPT}"
1535+
1536+
mkdir -p "$(dirname "$ROLLBACK_SCRIPT")"
1537+
backup_path "$ROLLBACK_SCRIPT"
1538+
1539+
cat > "$ROLLBACK_SCRIPT" <<'ROLLBACKBODY'
1540+
#!/bin/bash
1541+
set -euo pipefail
1542+
1543+
BACKUP_ROOT="/var/backups/uki-setup"
1544+
MANIFEST_DIR="${BACKUP_ROOT}/manifests"
1545+
DRY_RUN=0
1546+
TARGET_TS=""
1547+
1548+
RED='\e[31;1m'; GRN='\e[32;1m'; YLW='\e[33;1m'; RST='\e[0m'
1549+
info() { echo -e "${GRN}[uki-rollback]${RST} $*"; }
1550+
warn() { echo -e "${YLW}[uki-rollback]${RST} $*" >&2; }
1551+
die() { echo -e "${RED}[uki-rollback]${RST} $*" >&2; exit 1; }
1552+
1553+
usage() {
1554+
cat <<'EOF'
1555+
Usage: uki-rollback.sh [--list] [--timestamp <UTC_TIMESTAMP>] [--dry-run]
1556+
1557+
Options:
1558+
--list List available backup timestamps.
1559+
--timestamp <timestamp> Explicit backup timestamp to restore (required unless --list).
1560+
--dry-run Preview actions without modifying system state.
1561+
-h, --help Show this help output.
1562+
EOF
1563+
}
1564+
1565+
require_cmd() { command -v "$1" >/dev/null 2>&1 || die "Required command missing: $1"; }
1566+
1567+
run_or_echo() {
1568+
if [[ "$DRY_RUN" -eq 1 ]]; then
1569+
info "[dry-run] $*"
1570+
else
1571+
"$@"
1572+
fi
1573+
}
1574+
1575+
list_timestamps() {
1576+
[[ -d "$MANIFEST_DIR" ]] || { warn "No manifest directory found: ${MANIFEST_DIR}"; return 0; }
1577+
find "$MANIFEST_DIR" -maxdepth 1 -type f -name '*.manifest' -printf '%f\n' \
1578+
| sed 's/\.manifest$//' | sort
1579+
}
1580+
1581+
validate_manifest() {
1582+
local manifest="$1"
1583+
[[ -s "$manifest" ]] || die "Manifest is missing or empty: $manifest"
1584+
head -1 "$manifest" | grep -q '^# uki-setup manifest v1$' || die "Malformed manifest header in: $manifest"
1585+
}
1586+
1587+
restore_efi_from_snapshot() {
1588+
local snapshot="$1" line bootnum disk part label loader
1589+
[[ -f "$snapshot" ]] || { warn "EFI snapshot not found: $snapshot"; return 0; }
1590+
require_cmd efibootmgr
1591+
1592+
while IFS= read -r line; do
1593+
[[ "$line" =~ ^Boot([0-9A-Fa-f]{4})\*?[[:space:]]+([^[:space:]].*?)[[:space:]]+HD\(([0-9]+),GPT, ]] || continue
1594+
bootnum="${BASH_REMATCH[1]}"
1595+
label="${BASH_REMATCH[2]}"
1596+
part="${BASH_REMATCH[3]}"
1597+
loader=$(echo "$line" | sed -n 's/.*File(\\\(.*\))/\1/p')
1598+
[[ -n "$loader" ]] || continue
1599+
1600+
if efibootmgr | grep -q "^Boot${bootnum}"; then
1601+
info "EFI entry Boot${bootnum} already exists; skipping recreate."
1602+
continue
1603+
fi
1604+
1605+
disk=$(findmnt -n -o SOURCE /boot/efi 2>/dev/null || true)
1606+
[[ -b "$disk" ]] || { warn "Cannot determine ESP disk for EFI restore; skipping Boot${bootnum}."; continue; }
1607+
disk="/dev/$(lsblk -no PKNAME "$disk" 2>/dev/null | head -1)"
1608+
[[ -b "$disk" ]] || { warn "Cannot resolve parent disk for EFI restore; skipping Boot${bootnum}."; continue; }
1609+
1610+
if [[ "$DRY_RUN" -eq 1 ]]; then
1611+
info "[dry-run] efibootmgr --create --disk ${disk} --part ${part} --label ${label} --loader \\${loader}"
1612+
else
1613+
efibootmgr --quiet --create --disk "$disk" --part "$part" --label "$label" --loader "\\${loader}" || warn "Failed to recreate EFI entry Boot${bootnum}"
1614+
fi
1615+
done < "$snapshot"
1616+
}
1617+
1618+
restore_from_manifest() {
1619+
local manifest="$1" line op path backup extra
1620+
validate_manifest "$manifest"
1621+
1622+
while IFS= read -r line; do
1623+
[[ -z "$line" || "$line" =~ ^# || "$line" =~ ^timestamp= || "$line" =~ ^script= || "$line" =~ ^backup_dir= ]] && continue
1624+
IFS='|' read -r op path backup extra <<< "$line"
1625+
1626+
case "$op" in
1627+
modified)
1628+
[[ -n "$backup" && -e "$backup" ]] || { warn "Missing backup for modified path: $path"; continue; }
1629+
run_or_echo mkdir -p "$(dirname "$path")"
1630+
run_or_echo cp -a "$backup" "$path"
1631+
;;
1632+
created)
1633+
if [[ -e "$path" || -L "$path" ]]; then
1634+
run_or_echo rm -rf "$path"
1635+
fi
1636+
;;
1637+
deleted)
1638+
if [[ -n "$backup" && ( -e "$backup" || -L "$backup" ) ]]; then
1639+
run_or_echo mkdir -p "$(dirname "$path")"
1640+
run_or_echo cp -a "$backup" "$path"
1641+
else
1642+
warn "Cannot recreate deleted path without backup: $path"
1643+
fi
1644+
;;
1645+
efi_snapshot)
1646+
restore_efi_from_snapshot "$backup"
1647+
;;
1648+
*)
1649+
warn "Skipping malformed/unknown manifest entry: $line"
1650+
;;
1651+
esac
1652+
done < "$manifest"
1653+
}
1654+
1655+
while [[ $# -gt 0 ]]; do
1656+
case "$1" in
1657+
--list)
1658+
list_timestamps
1659+
exit 0
1660+
;;
1661+
--timestamp)
1662+
shift
1663+
[[ $# -gt 0 ]] || die "--timestamp requires a value"
1664+
TARGET_TS="$1"
1665+
;;
1666+
--dry-run)
1667+
DRY_RUN=1
1668+
;;
1669+
-h|--help)
1670+
usage
1671+
exit 0
1672+
;;
1673+
*)
1674+
die "Unknown option: $1"
1675+
;;
1676+
esac
1677+
shift
1678+
done
1679+
1680+
[[ $EUID -eq 0 ]] || die "Must run as root."
1681+
[[ -n "$TARGET_TS" ]] || die "Missing required --timestamp. Use --list to view available timestamps."
1682+
MANIFEST_PATH="${MANIFEST_DIR}/${TARGET_TS}.manifest"
1683+
[[ -f "$MANIFEST_PATH" ]] || die "Backup manifest not found: ${MANIFEST_PATH}"
1684+
1685+
info "Restoring backup timestamp: ${TARGET_TS}"
1686+
[[ "$DRY_RUN" -eq 1 ]] && info "Dry-run mode enabled."
1687+
restore_from_manifest "$MANIFEST_PATH"
1688+
info "Rollback complete."
1689+
ROLLBACKBODY
1690+
1691+
chmod 0755 "$ROLLBACK_SCRIPT"
1692+
record_created_path "$ROLLBACK_SCRIPT"
1693+
info "Rollback script written."
1694+
}
1695+
14261696
# =============================================================================
14271697
# PHASE 4 — Write kernel-install(8) plugin
14281698
# =============================================================================
@@ -1476,6 +1746,7 @@ exit 0
14761746
PLUGINBODY
14771747

14781748
chmod 0755 "$INSTALL_PLUGIN"
1749+
record_created_path "$INSTALL_PLUGIN"
14791750
info "Plugin written."
14801751
}
14811752

@@ -1507,6 +1778,7 @@ phase_disable_bls_plugins() {
15071778
backup_path "$target"
15081779
if [[ ! -e "$target" ]]; then
15091780
ln -s /dev/null "$target"
1781+
record_created_path "$target"
15101782
info " Disabled: ${p}"
15111783
else
15121784
info " Already overridden: ${p} — skipping."
@@ -1556,6 +1828,7 @@ phase_summary() {
15561828
echo " Files installed:"
15571829
echo " ${BUILD_SCRIPT} ← rebuild script (edit CMDLINE here)"
15581830
echo " ${INSTALL_PLUGIN} ← auto-trigger on kernel installs"
1831+
echo " ${ROLLBACK_SCRIPT} ← rollback helper"
15591832
echo ""
15601833
echo " UKIs are stored in: ${EFI_DIR}/"
15611834
find "${EFI_DIR}" -maxdepth 1 -type f -name "*.efi" -exec ls -lh {} + 2>/dev/null | sed 's/^/ /' || true
@@ -1618,9 +1891,11 @@ EOF
16181891
if [[ "${UKI_SETUP_SKIP_MAIN:-0}" -ne 1 ]]; then
16191892
parse_cli_args "$@"
16201893
[[ "$DRY_RUN" -eq 1 ]] && info "Dry-run mode enabled. Build script invocations will use --dry-run."
1894+
init_run_manifest
16211895
phase_preflight
16221896
phase_deps
16231897
phase_write_build_script
1898+
phase_write_rollback_script
16241899
phase_write_plugin
16251900
phase_disable_bls_plugins
16261901
phase_initial_build

0 commit comments

Comments
 (0)