Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
347c415aa7 | ||
|
|
45f957af23 |
@@ -1,5 +1,59 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v0.4.0 — 2026-07-13
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **`update.sh`** — fetch a newer release and apply it, preserving your
|
||||||
|
nested-snapshot opt-in setting.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash update.sh # to the newest release, with a confirmation
|
||||||
|
bash update.sh --check # show what would happen; change nothing
|
||||||
|
bash update.sh --rollback # undo the last update
|
||||||
|
```
|
||||||
|
|
||||||
|
**Run it by hand. Never from cron or a systemd timer.** This patch injects
|
||||||
|
Python into middlewared and re-applies itself at every boot, so an unattended
|
||||||
|
pull would let any bad upstream commit reach your box with no human in the loop
|
||||||
|
and take effect on the next reboot. v0.0.4 shipped exactly such a bug and took
|
||||||
|
every app on the box down. The manual step *is* the safety gate.
|
||||||
|
|
||||||
|
Design:
|
||||||
|
|
||||||
|
- **Defaults to the newest release tag, not `main`.** `main` can be mid-refactor;
|
||||||
|
a tag is the tested artifact. `--main` exists but says so loudly.
|
||||||
|
- Tags are ordered by **version**, not by date — date order silently downgrades
|
||||||
|
the box the first time a hotfix is tagged out of band (a v0.3.6 released after
|
||||||
|
v0.4.0 would sort as "newest").
|
||||||
|
- **Refuses to run over a dirty working tree** rather than merging across
|
||||||
|
hand-edited or scp'd files.
|
||||||
|
- Shows the commits you don't have and the target's release notes (read from the
|
||||||
|
*target's* CHANGELOG, via `tools/release_notes.py` — not a second copy of the
|
||||||
|
extractor), then asks before doing anything.
|
||||||
|
- **Records the previous revision before moving**, so `--rollback` works even if
|
||||||
|
`install.sh` dies halfway.
|
||||||
|
- Repairs `.git` ownership, which past `sudo git pull`s leave root-owned and
|
||||||
|
which then breaks every later non-root git command.
|
||||||
|
|
||||||
|
- `update.sh` is covered by the version-drift check, so it cannot quietly go stale
|
||||||
|
the way `create_task.py.__version__` did.
|
||||||
|
|
||||||
|
## v0.3.5 — 2026-07-13
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- `delete_snapshot_tree` swallowed the error from its recursive-delete fast path.
|
||||||
|
That failure is *usually* just "parent already gone" — stock's `finally` winning
|
||||||
|
the race once our mounts are released, which the by-name sweep then handles. But
|
||||||
|
if the cause were anything else, this was the only place it was visible, and it
|
||||||
|
went straight to `/dev/null`. It is now logged before falling through.
|
||||||
|
|
||||||
|
- Annotated the two remaining static-analysis findings as considered-and-accepted
|
||||||
|
rather than leaving them to be re-litigated: `subprocess` is always called in
|
||||||
|
list form (no shell, so ZFS dataset names cannot inject), and the partial
|
||||||
|
`systemctl` path is moot in a script that only runs as root.
|
||||||
|
|
||||||
## v0.3.4 — 2026-07-13
|
## v0.3.4 — 2026-07-13
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
@@ -341,27 +341,28 @@ Refresh your browser. S3 and B2 credentials now appear in the
|
|||||||
|
|
||||||
## Updating
|
## Updating
|
||||||
|
|
||||||
To update to a new version of the patch:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /mnt/tank/truenas-truecloud-patch
|
bash update.sh # to the newest release, with a confirmation
|
||||||
|
bash update.sh --check # show what would happen; change nothing
|
||||||
# If install.sh was previously run as root, the .git directory may be owned
|
bash update.sh --rollback # undo the last update
|
||||||
# by root. Fix it first, or just pull as root:
|
|
||||||
sudo git pull # easiest option
|
|
||||||
# — or —
|
|
||||||
sudo chown -R $(whoami) .git && git pull
|
|
||||||
|
|
||||||
bash install.sh
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`install.sh` clears any stale kill switch, re-applies the updated patches,
|
It preserves your nested-snapshot opt-in setting, shows you the commits and
|
||||||
and restarts middlewared. Run `python3 patch/create_task.py verify` afterwards
|
release notes you don't have yet, and asks before changing anything. It records
|
||||||
to confirm the patches loaded successfully.
|
the previous revision *before* moving, so `--rollback` works even if `install.sh`
|
||||||
|
dies halfway.
|
||||||
|
|
||||||
Check [CHANGELOG.md](CHANGELOG.md) to see what changed between versions.
|
**Run it by hand. Never from cron or a systemd timer.** This patch injects Python
|
||||||
|
into `middlewared` and re-applies itself at every boot, so an unattended pull would
|
||||||
|
let any bad upstream commit reach your box with no human in the loop and take
|
||||||
|
effect on the next reboot. v0.0.4 shipped exactly such a bug and took every app on
|
||||||
|
the box down. The manual step *is* the safety gate — if you want convenience, watch
|
||||||
|
the [releases](https://github.com/sudolulo/truenas-truecloud-patch/releases) feed,
|
||||||
|
don't automate the pull.
|
||||||
|
|
||||||
---
|
It updates to the newest **release tag**, not `main` — `main` can be mid-refactor,
|
||||||
|
and a tag is the tested artifact. `--main` exists if you want unreleased code, and
|
||||||
|
says so loudly.
|
||||||
|
|
||||||
## Creating a task via CLI
|
## Creating a task via CLI
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
VERSION="0.3.4"
|
VERSION="0.4.0"
|
||||||
|
|
||||||
# The directory containing install.sh is the permanent install location.
|
# The directory containing install.sh is the permanent install location.
|
||||||
PATCH_DIR="$(cd "$(dirname "$0")" && pwd)"
|
PATCH_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
@@ -96,7 +96,7 @@ fi
|
|||||||
|
|
||||||
echo "Setting permissions ..."
|
echo "Setting permissions ..."
|
||||||
chmod +x "$PATCH_DIR/patch/apply.sh" "$PATCH_DIR/patch/create_task.py" \
|
chmod +x "$PATCH_DIR/patch/apply.sh" "$PATCH_DIR/patch/create_task.py" \
|
||||||
"$PATCH_DIR/recover.sh" "$PATCH_DIR/uninstall.sh"
|
"$PATCH_DIR/recover.sh" "$PATCH_DIR/uninstall.sh" "$PATCH_DIR/update.sh"
|
||||||
echo "Done."
|
echo "Done."
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -32,7 +32,7 @@
|
|||||||
# Derive PATCH_DIR from this script's location (parent of the patch/ directory).
|
# Derive PATCH_DIR from this script's location (parent of the patch/ directory).
|
||||||
PATCH_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
PATCH_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
LOG="$PATCH_DIR/apply.log"
|
LOG="$PATCH_DIR/apply.log"
|
||||||
VERSION="0.3.4"
|
VERSION="0.4.0"
|
||||||
|
|
||||||
# Rotate log at 512 KB to avoid unbounded growth on a system volume.
|
# Rotate log at 512 KB to avoid unbounded growth on a system volume.
|
||||||
# Keep two prior generations (.1 and .2) so the last three boots are always available.
|
# Keep two prior generations (.1 and .2) so the last three boots are always available.
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ import subprocess
|
|||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
|
||||||
__version__ = "0.3.4"
|
__version__ = "0.4.0"
|
||||||
|
|
||||||
_PATCH_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
_PATCH_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
_STATUS_FILE = os.path.join(_PATCH_DIR, "hook_status.json")
|
_STATUS_FILE = os.path.join(_PATCH_DIR, "hook_status.json")
|
||||||
@@ -95,8 +95,11 @@ def midclt_call(method, *args):
|
|||||||
def _middlewared_start_epoch():
|
def _middlewared_start_epoch():
|
||||||
"""Epoch timestamp of the running middlewared main process, or None."""
|
"""Epoch timestamp of the running middlewared main process, or None."""
|
||||||
try:
|
try:
|
||||||
|
# Partial path (S607) is fine here: this runs as root on TrueNAS, so an
|
||||||
|
# attacker who can poison PATH already has root. Hard-coding a path would
|
||||||
|
# be less portable (/bin vs /usr/bin) for no security gain.
|
||||||
pid = int(subprocess.run(
|
pid = int(subprocess.run(
|
||||||
["systemctl", "show", "--property=MainPID", "--value", "middlewared"],
|
["systemctl", "show", "--property=MainPID", "--value", "middlewared"], # noqa: S607
|
||||||
capture_output=True, text=True, timeout=10, check=True,
|
capture_output=True, text=True, timeout=10, check=True,
|
||||||
).stdout.strip())
|
).stdout.strip())
|
||||||
if pid <= 0:
|
if pid <= 0:
|
||||||
|
|||||||
@@ -279,7 +279,11 @@ def current_mounts_under(root, mounts_file="/proc/self/mounts"):
|
|||||||
|
|
||||||
|
|
||||||
def _run(cmd):
|
def _run(cmd):
|
||||||
return subprocess.run(cmd, capture_output=True, text=True, check=False)
|
# List form, never shell=True: `cmd` is built from our own mount plan, so ZFS
|
||||||
|
# dataset names cannot inject. Runs as root by definition (it mounts).
|
||||||
|
return subprocess.run( # noqa: S603
|
||||||
|
cmd, capture_output=True, text=True, check=False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def apply_plan(mounts, runner=_run, isdir=os.path.isdir):
|
def apply_plan(mounts, runner=_run, isdir=os.path.isdir):
|
||||||
@@ -379,8 +383,16 @@ async def delete_snapshot_tree(middleware, snapshot, logger=None):
|
|||||||
try:
|
try:
|
||||||
await middleware.call("zfs.snapshot.delete", snapshot, {"recursive": True})
|
await middleware.call("zfs.snapshot.delete", snapshot, {"recursive": True})
|
||||||
return
|
return
|
||||||
except Exception: # noqa: BLE001 - fall through to the explicit sweep
|
except Exception as e: # noqa: BLE001 - fall through to the explicit sweep
|
||||||
pass
|
# Usually just "parent already gone" (stock's finally won the race once our
|
||||||
|
# mounts were released), which the sweep below handles. Log it rather than
|
||||||
|
# swallow it: if the real cause is something else, this is the only place
|
||||||
|
# it is visible -- the sweep would report a different, downstream failure.
|
||||||
|
if logger:
|
||||||
|
logger.debug(
|
||||||
|
"truecloud-patch: recursive delete of %s failed (%r); sweeping "
|
||||||
|
"the tree by name instead", snapshot, e,
|
||||||
|
)
|
||||||
|
|
||||||
# The parent may already be gone -- stock's `finally` can win the race once
|
# The parent may already be gone -- stock's `finally` can win the race once
|
||||||
# our mounts are released -- which fails the recursive delete while the
|
# our mounts are released -- which fails the recursive delete while the
|
||||||
|
|||||||
+1
-1
@@ -17,7 +17,7 @@
|
|||||||
# bash /mnt/tank/truenas-truecloud-patch/patch/apply.sh
|
# bash /mnt/tank/truenas-truecloud-patch/patch/apply.sh
|
||||||
# systemctl restart middlewared
|
# systemctl restart middlewared
|
||||||
|
|
||||||
VERSION="0.3.4"
|
VERSION="0.4.0"
|
||||||
|
|
||||||
PATCH_DIR="$(cd "$(dirname "$0")" && pwd)"
|
PATCH_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ VERSIONED_FILES = [
|
|||||||
"recover.sh",
|
"recover.sh",
|
||||||
os.path.join("patch", "apply.sh"),
|
os.path.join("patch", "apply.sh"),
|
||||||
os.path.join("patch", "create_task.py"), # exposes `--version` to users
|
os.path.join("patch", "create_task.py"), # exposes `--version` to users
|
||||||
|
"update.sh",
|
||||||
]
|
]
|
||||||
|
|
||||||
# `VERSION="x"` (shell) or `__version__ = "x"` (python).
|
# `VERSION="x"` (shell) or `__version__ = "x"` (python).
|
||||||
@@ -139,7 +140,11 @@ def main(argv):
|
|||||||
version = argv[2]
|
version = argv[2]
|
||||||
|
|
||||||
if cmd == "notes":
|
if cmd == "notes":
|
||||||
with open(CHANGELOG, encoding="utf-8") as fh:
|
# An explicit path lets update.sh show the notes from the CHANGELOG of the
|
||||||
|
# version it is about to install (`git show <tag>:CHANGELOG.md`), not the
|
||||||
|
# one already checked out.
|
||||||
|
path = argv[3] if len(argv) > 3 else CHANGELOG
|
||||||
|
with open(path, encoding="utf-8") as fh:
|
||||||
print(extract_notes(fh.read(), version))
|
print(extract_notes(fh.read(), version))
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
VERSION="0.3.4"
|
VERSION="0.4.0"
|
||||||
|
|
||||||
PATCH_DIR="$(cd "$(dirname "$0")" && pwd)"
|
PATCH_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
_HOOK_COMMENT='TrueCloud provider patch (S3/B2)'
|
_HOOK_COMMENT='TrueCloud provider patch (S3/B2)'
|
||||||
|
|||||||
@@ -0,0 +1,222 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# update.sh — fetch a newer release of truecloud-patch and apply it.
|
||||||
|
#
|
||||||
|
# ── RUN THIS BY HAND. NEVER FROM CRON OR A SYSTEMD TIMER. ─────────────────────
|
||||||
|
#
|
||||||
|
# This patch injects Python into middlewared and re-applies itself at every boot.
|
||||||
|
# An unattended pull would let any bad upstream commit reach your box with no
|
||||||
|
# human in the loop, and take effect on the next reboot. That is not theoretical:
|
||||||
|
# v0.0.4 shipped a boot-time bug that took every app on the box down.
|
||||||
|
#
|
||||||
|
# The manual step IS the safety gate. Keep it.
|
||||||
|
#
|
||||||
|
# By default this updates to the newest RELEASE TAG, not to main. main can be
|
||||||
|
# mid-refactor; a tag is the tested artifact. Use --main only if you know why.
|
||||||
|
#
|
||||||
|
# bash update.sh # to the newest release, with a confirmation
|
||||||
|
# bash update.sh --check # show what would happen; change nothing
|
||||||
|
# bash update.sh --rollback # undo the last update
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
VERSION="0.4.0"
|
||||||
|
|
||||||
|
PATCH_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
_PREV_FILE="$PATCH_DIR/.update_previous"
|
||||||
|
|
||||||
|
_target=""
|
||||||
|
_use_main=0
|
||||||
|
_assume_yes=0
|
||||||
|
_check_only=0
|
||||||
|
_rollback=0
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<USAGE
|
||||||
|
Usage: bash update.sh [options]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--to <ref> Update to a specific tag or commit (default: newest release tag)
|
||||||
|
--main Update to origin/main — UNRELEASED code, no guarantees
|
||||||
|
--check Show what an update would do and exit; changes nothing
|
||||||
|
--rollback Return to the revision recorded before the last update
|
||||||
|
--yes, -y Skip the confirmation prompt
|
||||||
|
-h, --help Show this help
|
||||||
|
|
||||||
|
Updating preserves your nested-snapshot opt-in setting either way.
|
||||||
|
USAGE
|
||||||
|
}
|
||||||
|
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--to) _target="${2:-}"; shift ;;
|
||||||
|
--main) _use_main=1 ;;
|
||||||
|
--check) _check_only=1 ;;
|
||||||
|
--rollback) _rollback=1 ;;
|
||||||
|
--yes|-y) _assume_yes=1 ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
*) echo "ERROR: unknown option: $1" >&2; echo "" >&2; usage >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "=== TrueNAS TrueCloud Provider Patch — Update (v${VERSION}) ==="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# ── Preflight ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
if [ "$(id -u)" -ne 0 ]; then
|
||||||
|
echo "ERROR: must be run as root (install.sh needs it)." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd "$PATCH_DIR"
|
||||||
|
|
||||||
|
if ! git rev-parse --git-dir >/dev/null 2>&1; then
|
||||||
|
echo "ERROR: $PATCH_DIR is not a git clone — nothing to update." >&2
|
||||||
|
echo " Re-clone from https://github.com/sudolulo/truenas-truecloud-patch" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Past `sudo git pull`s can leave root-owned objects in .git that then break any
|
||||||
|
# non-root git command. We run as root, so we would only make that worse.
|
||||||
|
_owner="$(stat -c '%U' "$PATCH_DIR")"
|
||||||
|
if [ -n "$_owner" ] && [ "$_owner" != "root" ]; then
|
||||||
|
chown -R "$_owner" "$PATCH_DIR/.git" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# A dirty tree means someone edited or scp'd files in place; merging over that
|
||||||
|
# silently loses their changes, or conflicts halfway through.
|
||||||
|
if [ -n "$(git status --porcelain --untracked-files=no)" ]; then
|
||||||
|
echo "ERROR: the working tree has uncommitted changes:" >&2
|
||||||
|
git status --short --untracked-files=no >&2
|
||||||
|
echo "" >&2
|
||||||
|
echo " Refusing to update over them. Commit, stash, or discard them first:" >&2
|
||||||
|
echo " git -C $PATCH_DIR checkout -- ." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Rollback ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
if [ "$_rollback" -eq 1 ]; then
|
||||||
|
if [ ! -f "$_PREV_FILE" ]; then
|
||||||
|
echo "ERROR: no previous revision recorded — nothing to roll back to." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
_prev="$(cat "$_PREV_FILE")"
|
||||||
|
echo "Rolling back to $_prev ..."
|
||||||
|
git checkout -q "$_prev"
|
||||||
|
echo "Reverted. Re-applying ..."
|
||||||
|
echo ""
|
||||||
|
bash "$PATCH_DIR/install.sh"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Work out where we are and where we are going ──────────────────────────────
|
||||||
|
|
||||||
|
echo "Fetching ..."
|
||||||
|
git fetch --quiet --tags --prune origin
|
||||||
|
|
||||||
|
_current="$(git rev-parse HEAD)"
|
||||||
|
_current_desc="$(git describe --tags --always 2>/dev/null || echo "$_current")"
|
||||||
|
|
||||||
|
if [ -n "$_target" ]; then
|
||||||
|
:
|
||||||
|
elif [ "$_use_main" -eq 1 ]; then
|
||||||
|
_target="origin/main"
|
||||||
|
else
|
||||||
|
# Newest release tag by VERSION order, not by tag date. Date order is only
|
||||||
|
# correct while tags are created in ascending version order; it breaks the
|
||||||
|
# moment a hotfix is tagged out of band (a v0.3.6 released after v0.4.0 would
|
||||||
|
# sort as "newest" by date and silently downgrade the box).
|
||||||
|
_target="$(git tag -l 'v*' --sort=-version:refname | head -1)"
|
||||||
|
if [ -z "$_target" ]; then
|
||||||
|
echo "ERROR: no release tags found; use --main to track unreleased code." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! _target_sha="$(git rev-parse --verify --quiet "${_target}^{commit}")"; then
|
||||||
|
echo "ERROR: '$_target' is not a valid tag, branch, or commit." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo " current: $_current_desc"
|
||||||
|
echo " target: $_target ($(git rev-parse --short "$_target_sha"))"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [ "$_current" = "$_target_sha" ]; then
|
||||||
|
echo "Already up to date. Nothing to do."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Show what is coming ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
echo "Commits you do not have yet:"
|
||||||
|
git log --oneline --no-decorate "$_current..$_target_sha" | sed 's/^/ /' || true
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Reuse tools/release_notes.py rather than re-implementing the extractor here —
|
||||||
|
# a second copy would be the untested one. Read the CHANGELOG *of the target*, so
|
||||||
|
# the notes describe what you are about to install.
|
||||||
|
if [ -f "$PATCH_DIR/tools/release_notes.py" ] && [ "$_use_main" -eq 0 ] \
|
||||||
|
&& [ -z "${_target##v*}" ]; then
|
||||||
|
_cl="$(mktemp)"
|
||||||
|
if git show "$_target_sha:CHANGELOG.md" > "$_cl" 2>/dev/null && [ -s "$_cl" ]; then
|
||||||
|
echo "Release notes for $_target:"
|
||||||
|
python3 "$PATCH_DIR/tools/release_notes.py" notes "$_target" "$_cl" \
|
||||||
|
2>/dev/null | sed 's/^/ /' || echo " (no notes for $_target)"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
rm -f "$_cl"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$_use_main" -eq 1 ]; then
|
||||||
|
echo "NOTE: --main tracks UNRELEASED code. It has passed CI, but it is not a"
|
||||||
|
echo " tested release, and apply.sh runs at every boot."
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$_check_only" -eq 1 ]; then
|
||||||
|
echo "--check given; nothing changed."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Confirm ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
if [ "$_assume_yes" -eq 0 ]; then
|
||||||
|
printf "Apply this update and restart middlewared? [y/N] "
|
||||||
|
read -r _answer </dev/tty || _answer=""
|
||||||
|
case "$_answer" in
|
||||||
|
y|Y|yes|YES) ;;
|
||||||
|
*) echo "Aborted. Nothing changed."; exit 0 ;;
|
||||||
|
esac
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Apply ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# Record where we were BEFORE moving, so --rollback works even if install.sh dies.
|
||||||
|
echo "$_current" > "$_PREV_FILE"
|
||||||
|
|
||||||
|
echo "Checking out $_target ..."
|
||||||
|
git checkout -q --detach "$_target_sha"
|
||||||
|
echo " now at $(git describe --tags --always)"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "Applying (this preserves your nested-snapshot setting) ..."
|
||||||
|
echo ""
|
||||||
|
if ! bash "$PATCH_DIR/install.sh"; then
|
||||||
|
echo ""
|
||||||
|
echo "ERROR: install.sh failed after updating." >&2
|
||||||
|
echo " Roll back with: bash $PATCH_DIR/update.sh --rollback" >&2
|
||||||
|
echo " Or disable the patch entirely: bash $PATCH_DIR/recover.sh" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Update complete ==="
|
||||||
|
echo " $_current_desc -> $(git describe --tags --always)"
|
||||||
|
echo ""
|
||||||
|
echo "If anything looks wrong:"
|
||||||
|
echo " bash $PATCH_DIR/update.sh --rollback # back to $_current_desc"
|
||||||
|
echo " bash $PATCH_DIR/recover.sh # kill switch + restart"
|
||||||
Reference in New Issue
Block a user