v0.28.0
This commit is contained in:
1 parent
c24bc4279b
commit
3b3f0db377
84 files changed
+2686
-392
No files matched your search
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
from deploy_core import * # re-exported internal deployment primitives
|
||||
|
||||
ACME_RUNTIME_NAMES = frozenset({'acme-python', 'acme-python.installing', 'acme-python.virtualenv-bootstrap'})
|
||||
RUNTIME_STATE_NAMES = frozenset({'.geoip-update.lock'})
|
||||
CERTBOT_PACKAGES = ('certbot==5.8.0', 'certbot-dns-cloudflare==5.8.0', 'certbot-dns-ovh==5.8.0', 'certbot-dns-rfc2136==5.8.0')
|
||||
|
||||
def _under_acme_runtime(source: Path, path: Path) -> bool:
|
||||
@@ -30,7 +31,9 @@ def copy_data(source: Path, target: Path) -> None:
|
||||
checked_tree(source)
|
||||
root = source.resolve(strict=True)
|
||||
def ignore(directory: str, names: list[str]) -> set[str]:
|
||||
return set(names) & ACME_RUNTIME_NAMES if Path(directory).resolve(strict=True) == root else set()
|
||||
if Path(directory).resolve(strict=True) != root:
|
||||
return set()
|
||||
return set(names) & (ACME_RUNTIME_NAMES | RUNTIME_STATE_NAMES)
|
||||
shutil.copytree(source, target, symlinks=True, ignore=ignore)
|
||||
os.chmod(target, 0o700)
|
||||
|
||||
@@ -353,64 +356,68 @@ def stop_for_managed_update(data: Path) -> None:
|
||||
|
||||
def update(record: dict, binary: Path, sha: str, start: bool, refresh_geoip: bool = False, reset_config: bool = False) -> dict:
|
||||
prefix = Path(record['prefix']); data = Path(record['data_dir']); account = pwd.getpwnam(record['user'])
|
||||
# Updates deliberately do not rewrite shell environment. Check the installer-
|
||||
# managed PATH profile early and report the exact remediation target instead.
|
||||
check_shell_path(prefix)
|
||||
# Do this before staging/network work and, critically, before stopping the
|
||||
# currently healthy service. It also repairs stale root:root/0700 metadata
|
||||
# left by manual maintenance while preserving private 0700 semantics.
|
||||
ensure_service_data_access(data, account)
|
||||
release, version = stage(binary, prefix, record['user'], sha)
|
||||
geoip_lock_fd: int | None = None
|
||||
geoip_stage: Path | None = None
|
||||
geoip_names: list[str] = []
|
||||
if refresh_geoip:
|
||||
geoip_stage, geoip_names = prepare_geoip_update(data, prefix)
|
||||
active = subprocess.run(['systemctl', 'is-active', '--quiet', SERVICE], check=False, stdout=subprocess.DEVNULL).returncode == 0
|
||||
stop_for_managed_update(data)
|
||||
try:
|
||||
backup = backup_stopped(record, prefix)
|
||||
except BaseException:
|
||||
if active: run(['systemctl', 'start', SERVICE])
|
||||
if geoip_stage is not None:
|
||||
shutil.rmtree(geoip_stage, ignore_errors=True)
|
||||
raise
|
||||
replacement = dict(record, release=release, version=version, sha256=sha)
|
||||
try:
|
||||
if reset_config:
|
||||
reset_saved_config(data)
|
||||
apply_geoip_update(data, geoip_stage, geoip_names, account.pw_uid, account.pw_gid)
|
||||
prepare_managed_acme_runtime(data, account)
|
||||
switch(prefix, release)
|
||||
write_private(UNIT_PATH, service_unit(replacement).encode(), 0o644)
|
||||
run(['systemctl', 'daemon-reload'])
|
||||
if start:
|
||||
run(['systemctl', 'start', SERVICE])
|
||||
if not healthy(replacement, version):
|
||||
raise RuntimeError('New release failed the local health/version check.')
|
||||
write_private(prefix / 'install.json', json.dumps(replacement, indent=2).encode())
|
||||
except BaseException as error:
|
||||
details = service_failure_details()
|
||||
if details:
|
||||
print('New release diagnostics:\n' + details, file=sys.stderr)
|
||||
# Updates deliberately do not rewrite shell environment. Check the installer-
|
||||
# managed PATH profile early and report the exact remediation target instead.
|
||||
check_shell_path(prefix)
|
||||
# Repair stale root:root/0700 metadata before staging/network work and before
|
||||
# stopping the currently healthy service.
|
||||
ensure_service_data_access(data, account)
|
||||
release, version = stage(binary, prefix, record['user'], sha)
|
||||
geoip_names: list[str] = []
|
||||
if refresh_geoip:
|
||||
# Keep the same lock for checksum/download staging and publication. The
|
||||
# runtime worker uses this file too, so manual upgrades cannot race it.
|
||||
geoip_lock_fd = acquire_geoip_update_lock(data, account.pw_uid, account.pw_gid)
|
||||
geoip_stage, geoip_names = prepare_geoip_update(data, prefix)
|
||||
active = subprocess.run(['systemctl', 'is-active', '--quiet', SERVICE], check=False, stdout=subprocess.DEVNULL).returncode == 0
|
||||
stop_for_managed_update(data)
|
||||
failed = data.with_name(data.name + '.failed-' + uuid.uuid4().hex[:12])
|
||||
data.rename(failed) # keep the failed generation for diagnosis; do not delete it
|
||||
copy_data(backup / 'data', data)
|
||||
restore_managed_acme_runtime(failed, data)
|
||||
chown_tree(data, account.pw_uid, account.pw_gid)
|
||||
switch(prefix, record['release'])
|
||||
restore_service_unit(backup)
|
||||
write_private(prefix / 'install.json', json.dumps(record, indent=2).encode())
|
||||
if active:
|
||||
run(['systemctl', 'start', SERVICE])
|
||||
if not healthy(record, record['version']):
|
||||
raise RuntimeError(f'Update failed: {error}. Rollback restored code/data/unit but previous service health also failed. Backup: {backup}; failed state: {failed}') from error
|
||||
raise RuntimeError(f'Update failed: {error}. Previous code, private data and service unit were restored. Installation is still present; do not run install.sh. Fix the cause and rerun update.sh. Backup: {backup}; failed state: {failed}') from error
|
||||
try:
|
||||
backup = backup_stopped(record, prefix)
|
||||
except BaseException:
|
||||
if active:
|
||||
run(['systemctl', 'start', SERVICE])
|
||||
raise
|
||||
replacement = dict(record, release=release, version=version, sha256=sha)
|
||||
try:
|
||||
if reset_config:
|
||||
reset_saved_config(data)
|
||||
apply_geoip_update(data, geoip_stage, geoip_names, account.pw_uid, account.pw_gid)
|
||||
prepare_managed_acme_runtime(data, account)
|
||||
switch(prefix, release)
|
||||
write_private(UNIT_PATH, service_unit(replacement).encode(), 0o644)
|
||||
run(['systemctl', 'daemon-reload'])
|
||||
if start:
|
||||
run(['systemctl', 'start', SERVICE])
|
||||
if not healthy(replacement, version):
|
||||
raise RuntimeError('New release failed the local health/version check.')
|
||||
write_private(prefix / 'install.json', json.dumps(replacement, indent=2).encode())
|
||||
except BaseException as error:
|
||||
details = service_failure_details()
|
||||
if details:
|
||||
print('New release diagnostics:\n' + details, file=sys.stderr)
|
||||
stop_for_managed_update(data)
|
||||
failed = data.with_name(data.name + '.failed-' + uuid.uuid4().hex[:12])
|
||||
data.rename(failed) # keep the failed generation for diagnosis; do not delete it
|
||||
copy_data(backup / 'data', data)
|
||||
restore_managed_acme_runtime(failed, data)
|
||||
chown_tree(data, account.pw_uid, account.pw_gid)
|
||||
switch(prefix, record['release'])
|
||||
restore_service_unit(backup)
|
||||
write_private(prefix / 'install.json', json.dumps(record, indent=2).encode())
|
||||
if active:
|
||||
run(['systemctl', 'start', SERVICE])
|
||||
if not healthy(record, record['version']):
|
||||
raise RuntimeError(f'Update failed: {error}. Rollback restored code/data/unit but previous service health also failed. Backup: {backup}; failed state: {failed}') from error
|
||||
raise RuntimeError(f'Update failed: {error}. Previous code, private data and service unit were restored. Installation is still present; do not run install.sh. Fix the cause and rerun update.sh. Backup: {backup}; failed state: {failed}') from error
|
||||
print(f'Backup (contains private keys): {backup}')
|
||||
return replacement
|
||||
finally:
|
||||
if geoip_stage is not None:
|
||||
shutil.rmtree(geoip_stage, ignore_errors=True)
|
||||
print(f'Backup (contains private keys): {backup}')
|
||||
return replacement
|
||||
release_geoip_update_lock(geoip_lock_fd)
|
||||
|
||||
|
||||
def install(record: dict, binary: Path, sha: str, start: bool, install_geoip: bool = False) -> dict:
|
||||
|
||||
Reference in new issue
Block a user