v0.3.4: one implementation of apply/revert (patch/mw_patch.py)
The "strip the TRUECLOUD_PATCH block" logic existed twice -- in apply.sh's heredoc and in an inline heredoc in uninstall.sh -- and the uninstall copy was the untested one. That is precisely how the two could have drifted apart, with apply.sh reverting one set of files and uninstall.sh another. Both now call patch/mw_patch.py. 17 new tests cover it, including that revert_nested never touches restic.py: that file carries a TRUECLOUD_PATCH block too, but it belongs to the providers module, and removing it would silently break B2 backups. apply.sh imports it fail-safe -- on ImportError the backend patch is skipped and middlewared starts stock, which is this script's whole design principle. The import uses sys.path.append, never insert(0): prepending would give patch/ precedence over the stdlib for that interpreter, so a future patch/json.py would shadow the real json module and break the boot. Also: the README's create_task.py example still taught `--password <secret>`, which is how a security fix quietly fails to land. It now shows --password-stdin. 132 tests, ruff and shellcheck clean.
This commit is contained in:
+19
-60
@@ -32,7 +32,7 @@
|
||||
# Derive PATCH_DIR from this script's location (parent of the patch/ directory).
|
||||
PATCH_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
LOG="$PATCH_DIR/apply.log"
|
||||
VERSION="0.3.3"
|
||||
VERSION="0.3.4"
|
||||
|
||||
# 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.
|
||||
@@ -468,65 +468,24 @@ if _tc_nested is not None:
|
||||
"""
|
||||
|
||||
|
||||
def patch_file(path, block):
|
||||
with open(path, encoding="utf-8") as fh:
|
||||
content = fh.read()
|
||||
marker = "\n# TRUECLOUD_PATCH"
|
||||
idx = content.find(marker)
|
||||
base = content[:idx] if idx != -1 else content
|
||||
with open(path, "w", encoding="utf-8") as fh:
|
||||
fh.write(base.rstrip("\n") + "\n" + block)
|
||||
# Single implementation of the block apply/revert logic (patch/mw_patch.py), so
|
||||
# uninstall.sh and apply.sh cannot drift apart. Fail-safe: if it cannot be
|
||||
# imported, skip the backend patch entirely -- middlewared then starts stock,
|
||||
# which is the whole design principle of this script.
|
||||
# APPEND, never insert(0): this dir would otherwise take precedence over the
|
||||
# stdlib for this interpreter, so a future patch/json.py (say) would shadow the
|
||||
# real json module and break the boot. Appending fails safe -- worst case our
|
||||
# import misses and the backend patch is skipped.
|
||||
sys.path.append(os.path.dirname(nested_src))
|
||||
try:
|
||||
from mw_patch import patch_file, revert_nested
|
||||
except ImportError as _e:
|
||||
print(f'WARNING: cannot import patch/mw_patch.py ({_e}) — skipping backend patch.')
|
||||
print('WARNING: middlewared will start with stock (unpatched) modules.')
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def unpatch_file(path):
|
||||
"""Strip our appended block, restoring the stock file. True if it was patched."""
|
||||
try:
|
||||
with open(path, encoding="utf-8") as fh:
|
||||
content = fh.read()
|
||||
except OSError:
|
||||
return False
|
||||
idx = content.find("\n# TRUECLOUD_PATCH")
|
||||
if idx == -1:
|
||||
return False
|
||||
try:
|
||||
with open(path, "w", encoding="utf-8") as fh:
|
||||
fh.write(content[:idx].rstrip("\n") + "\n")
|
||||
except OSError:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def revert_nested(cloud_dir, sync_path):
|
||||
"""Undo the nested patch. Returns the names of what was actually reverted.
|
||||
|
||||
Skipping the patch is NOT enough to disable the feature. The overlay persists
|
||||
for the whole boot, so an earlier run this boot may already have written the
|
||||
patched files -- and middlewared re-imports them on the restart that
|
||||
install.sh performs. Without this, `install.sh --disable-nested-snapshots`
|
||||
would report "disabled" while the feature kept running until the next reboot.
|
||||
"""
|
||||
reverted = []
|
||||
|
||||
# Remove the module FIRST. Every injected block is guarded by
|
||||
# `if _tc_nested is not None`, so once it is gone they all no-op even if a
|
||||
# later step here fails -- the guard is restored no matter what.
|
||||
try:
|
||||
os.unlink(os.path.join(cloud_dir, '_truecloud_nested.py'))
|
||||
reverted.append('_truecloud_nested.py')
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
# NB: restic.py also carries a TRUECLOUD_PATCH block, but that belongs to the
|
||||
# providers module. Only these three are ours to revert.
|
||||
for name, path in (
|
||||
('crud.py', os.path.join(cloud_dir, 'crud.py')),
|
||||
('sync.py', sync_path),
|
||||
('snapshot.py', os.path.join(cloud_dir, 'snapshot.py')),
|
||||
):
|
||||
if unpatch_file(path):
|
||||
reverted.append(name)
|
||||
|
||||
return reverted
|
||||
# .../middlewared/plugins/cloud -> .../middlewared
|
||||
mw_dir = os.path.dirname(os.path.dirname(cloud_dir))
|
||||
|
||||
b2_ok = restic_ok = False
|
||||
nested_ok = False
|
||||
@@ -582,7 +541,7 @@ if not nested_needed:
|
||||
nested_detail = 'not needed'
|
||||
print('INFO: Nested module skipped.')
|
||||
|
||||
reverted = revert_nested(cloud_dir, sync_path)
|
||||
reverted = revert_nested(mw_dir)
|
||||
if reverted:
|
||||
print('OK: Reverted a previously-applied nested patch (' + ', '.join(reverted) + ').')
|
||||
print(' The stock nesting guard is restored once middlewared restarts.')
|
||||
|
||||
@@ -52,7 +52,7 @@ import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
__version__ = "0.3.3"
|
||||
__version__ = "0.3.4"
|
||||
|
||||
_PATCH_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
_STATUS_FILE = os.path.join(_PATCH_DIR, "hook_status.json")
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Apply and revert truecloud-patch's blocks in middlewared's modules.
|
||||
|
||||
Every patch this project makes to a middlewared module is an appended block that
|
||||
begins with the MARKER line. That makes patching idempotent (truncate at the
|
||||
marker, re-append) and reverting exact (truncate at the marker, stop).
|
||||
|
||||
This is the single implementation of that. It used to live in two places --
|
||||
apply.sh's heredoc and an inline heredoc in uninstall.sh -- and the uninstall copy
|
||||
was the untested one.
|
||||
|
||||
python3 mw_patch.py revert-all # remove every block + the nested module
|
||||
python3 mw_patch.py revert-nested # remove only the nested module's blocks
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
MARKER = "\n# TRUECLOUD_PATCH"
|
||||
|
||||
#: Modules the providers module (B2/S3) patches.
|
||||
PROVIDER_RELPATHS = [
|
||||
("rclone", "remote", "b2.py"),
|
||||
("plugins", "cloud_backup", "restic.py"),
|
||||
]
|
||||
|
||||
#: Modules the nested-snapshot module patches. Order matters on revert -- see
|
||||
#: revert(): the loadable module goes first.
|
||||
NESTED_RELPATHS = [
|
||||
("plugins", "cloud", "crud.py"),
|
||||
("plugins", "cloud_backup", "sync.py"),
|
||||
("plugins", "cloud", "snapshot.py"),
|
||||
]
|
||||
|
||||
#: The importable module the nested blocks depend on.
|
||||
NESTED_MODULE = ("plugins", "cloud", "_truecloud_nested.py")
|
||||
|
||||
|
||||
def patch_file(path, block):
|
||||
"""Append `block`, replacing any block we appended before. Idempotent."""
|
||||
with open(path, encoding="utf-8") as fh:
|
||||
content = fh.read()
|
||||
idx = content.find(MARKER)
|
||||
base = content[:idx] if idx != -1 else content
|
||||
with open(path, "w", encoding="utf-8") as fh:
|
||||
fh.write(base.rstrip("\n") + "\n" + block)
|
||||
|
||||
|
||||
def unpatch_file(path):
|
||||
"""Strip our appended block, restoring the stock file. True if it was patched."""
|
||||
try:
|
||||
with open(path, encoding="utf-8") as fh:
|
||||
content = fh.read()
|
||||
except OSError:
|
||||
return False
|
||||
idx = content.find(MARKER)
|
||||
if idx == -1:
|
||||
return False
|
||||
try:
|
||||
with open(path, "w", encoding="utf-8") as fh:
|
||||
fh.write(content[:idx].rstrip("\n") + "\n")
|
||||
except OSError:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def revert(mw_dir, relpaths, module_relpath=None):
|
||||
"""Remove our blocks from `relpaths`, and the module at `module_relpath`.
|
||||
|
||||
The module is deleted FIRST. Every injected block is guarded by
|
||||
`if _tc_nested is not None`, so once the module is gone the blocks all no-op
|
||||
even if a later unpatch fails -- the stock guard comes back regardless.
|
||||
|
||||
Returns the names of what was actually reverted.
|
||||
"""
|
||||
reverted = []
|
||||
|
||||
if module_relpath:
|
||||
try:
|
||||
os.unlink(os.path.join(mw_dir, *module_relpath))
|
||||
reverted.append(module_relpath[-1])
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
for rel in relpaths:
|
||||
if unpatch_file(os.path.join(mw_dir, *rel)):
|
||||
reverted.append(rel[-1])
|
||||
|
||||
return reverted
|
||||
|
||||
|
||||
def revert_nested(mw_dir):
|
||||
"""Undo the nested-snapshot patch only. Leaves the providers patch alone.
|
||||
|
||||
restic.py also carries a block, but it belongs to the providers module --
|
||||
reverting it would silently break B2 backups.
|
||||
"""
|
||||
return revert(mw_dir, NESTED_RELPATHS, NESTED_MODULE)
|
||||
|
||||
|
||||
def revert_all(mw_dir):
|
||||
"""Undo every patch this project applies."""
|
||||
return revert(mw_dir, NESTED_RELPATHS + PROVIDER_RELPATHS, NESTED_MODULE)
|
||||
|
||||
|
||||
def find_middlewared_dir():
|
||||
"""Directory of the installed `middlewared` package, or None."""
|
||||
try:
|
||||
import middlewared
|
||||
except ImportError:
|
||||
return None
|
||||
return os.path.dirname(os.path.abspath(middlewared.__file__))
|
||||
|
||||
|
||||
def main(argv):
|
||||
if len(argv) < 2 or argv[1] not in ("revert-all", "revert-nested"):
|
||||
print(__doc__, file=sys.stderr)
|
||||
return 2
|
||||
|
||||
mw_dir = find_middlewared_dir()
|
||||
if mw_dir is None:
|
||||
print(" middlewared not importable — nothing to revert.")
|
||||
return 0
|
||||
|
||||
fn = revert_all if argv[1] == "revert-all" else revert_nested
|
||||
reverted = fn(mw_dir)
|
||||
if reverted:
|
||||
print(" Reverted: " + ", ".join(reverted))
|
||||
else:
|
||||
print(" Nothing to revert (overlay already removed, or never patched).")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv))
|
||||
Reference in New Issue
Block a user