Add self-contained automated server upgrade package
This commit is contained in:
1 parent
4d58fc7b66
commit
8e6c9a058f
14 files changed
+908
-26
No files matched your search
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build a single self-extracting installer from an explicit, secret-free allowlist."""
|
||||
import argparse
|
||||
import gzip
|
||||
import hashlib
|
||||
import io
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
import tarfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
FILES = ['install.sh', 'update.sh', 'README.md', 'Dokumentation.md', 'UPDATE_ANLEITUNG.md',
|
||||
'lib/common.sh', 'lib/locking.sh', 'lib/backup_set.py', 'lib/update-discover.py',
|
||||
'usr-local-sbin/app-backup.sh', 'usr-local-sbin/app-restore.sh',
|
||||
'systemd/app-backup.service', 'systemd/app-backup.timer', 'logrotate/app-backup',
|
||||
'etc/app-backup/app-backup.conf']
|
||||
|
||||
|
||||
def build(output, version):
|
||||
if not re.fullmatch(r'[A-Za-z0-9._-]+', version):
|
||||
raise ValueError('Unsafe version identifier')
|
||||
files = FILES + [str(p.relative_to(ROOT)) for p in sorted((ROOT/'etc/app-backup').glob('*.cnf.example'))]
|
||||
buffer = io.BytesIO()
|
||||
with gzip.GzipFile(fileobj=buffer, mode='wb', mtime=0) as zipped:
|
||||
with tarfile.open(fileobj=zipped, mode='w') as archive:
|
||||
for name in sorted(files):
|
||||
path = ROOT / name
|
||||
if path.is_symlink() or not path.is_file():
|
||||
raise ValueError(f'Missing/unsafe package input: {name}')
|
||||
raw = path.read_bytes()
|
||||
info = tarfile.TarInfo(name)
|
||||
info.size = len(raw)
|
||||
info.mode = 0o700 if name.endswith('.sh') else 0o600
|
||||
archive.addfile(info, io.BytesIO(raw))
|
||||
raw = (version + '\n').encode()
|
||||
info = tarfile.TarInfo('VERSION')
|
||||
info.size = len(raw)
|
||||
info.mode = 0o600
|
||||
archive.addfile(info, io.BytesIO(raw))
|
||||
payload = buffer.getvalue()
|
||||
checksum = hashlib.sha256(payload).hexdigest()
|
||||
# Payload checksum catches transfer corruption. It is not a signature; obtain
|
||||
# this executable only through the trusted SSH/source repository channel.
|
||||
header = f'''#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
umask 077
|
||||
if [[ ${{1:-}} == --help ]]; then
|
||||
echo 'Usage: sudo bash app-backup-update-{version}.run [--foreground] [--config FILE] [--rclone-config FILE] [--enable-gitea | --no-gitea] [--cron-services UNIT,...] [--install-deps]'
|
||||
exit 0
|
||||
fi
|
||||
[[ $EUID == 0 ]] || {{ echo 'Bitte mit sudo bash starten.' >&2; exit 1; }}
|
||||
for tool in sha256sum tar mktemp tail awk; do command -v "$tool" >/dev/null; done
|
||||
line=$(awk '/^__APP_BACKUP_PAYLOAD__$/ {{print NR + 1; exit}}' "$0")
|
||||
[[ -n $line ]] || exit 1
|
||||
actual=$(tail -n +"$line" "$0" | sha256sum)
|
||||
[[ ${{actual%% *}} == {checksum} ]] || {{ echo 'Paket-Pruefsumme falsch; keine Installation.' >&2; exit 1; }}
|
||||
# Root-owned persistent extraction survives SSH disconnect and worker startup.
|
||||
package_dir=$(mktemp -d /var/tmp/app-backup-package-{version}-XXXXXXXX)
|
||||
tail -n +"$line" "$0" | tar -xz --no-same-owner -C "$package_dir"
|
||||
exec bash "$package_dir/update.sh" "$@"
|
||||
exit 1
|
||||
__APP_BACKUP_PAYLOAD__
|
||||
'''.encode()
|
||||
output.mkdir(parents=True, exist_ok=True)
|
||||
package = output / f'app-backup-update-{version}.run'
|
||||
package.write_bytes(header + payload)
|
||||
package.chmod(0o700)
|
||||
(output / (package.name+'.sha256')).write_text(hashlib.sha256(package.read_bytes()).hexdigest()+' '+package.name+'\n')
|
||||
return package
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--output', type=Path, default=ROOT/'dist')
|
||||
parser.add_argument('--version', help='Override for development/testing only')
|
||||
args = parser.parse_args()
|
||||
version = args.version
|
||||
if not version:
|
||||
if subprocess.check_output(['git','status','--porcelain'],cwd=ROOT).strip():
|
||||
parser.error('Commit source changes before creating a release package')
|
||||
version = subprocess.check_output(['git','rev-parse','--short=12','HEAD'],cwd=ROOT,text=True).strip()
|
||||
print(build(args.output, version))
|
||||
Reference in new issue
Block a user