v0.29.2
This commit is contained in:
1 parent
76cb4acef0
commit
68678e98be
84 files changed
+1519
-325
No files matched your search
@@ -85,7 +85,7 @@ def download(destination: Path) -> None:
|
||||
"""TLS verification uses Python's default trust store; never executes a shell."""
|
||||
started = time.monotonic()
|
||||
opener = build_opener(HttpsRedirects())
|
||||
request = Request(DOWNLOAD_URL, headers={"User-Agent": "RustProxy-build/0.29.0"})
|
||||
request = Request(DOWNLOAD_URL, headers={"User-Agent": "RustProxy-build"})
|
||||
with opener.open(request, timeout=30) as response, destination.open("wb") as output:
|
||||
length = response.headers.get("Content-Length")
|
||||
if length and int(length) > MAX_ARCHIVE:
|
||||
|
||||
@@ -50,7 +50,8 @@ def main() -> None:
|
||||
'start_service':not args.no_start,
|
||||
'geoip_action':'skip' if args.skip_geoip else ('download-and-verify' if args.mode == 'install' else 'verify-and-refresh-if-stale'),
|
||||
'shell_path_profile':str(SHELL_PATH_PROFILE),
|
||||
'shell_path_action':'install' if args.mode == 'install' else 'check-only',
|
||||
'shell_path_action':'install-or-repair',
|
||||
'logrotate_path':str(LOGROTATE_PATH), 'logrotate_action':'install-or-repair',
|
||||
'reset_saved_config':bool(args.reset_config), 'changes_made':False}, indent=2)); return
|
||||
if sys.platform != 'linux' or os.geteuid() != 0 or not shutil.which('systemctl') or not shutil.which('runuser'):
|
||||
raise ValueError('Installation requires root on Linux with systemd and runuser. Use --plan for inspection.')
|
||||
|
||||
@@ -34,6 +34,7 @@ import urllib.parse
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
UNIT_PATH = Path('/etc/systemd/system/rustproxy.service')
|
||||
SHELL_PATH_PROFILE = Path('/etc/profile.d/rustproxy-path.sh')
|
||||
LOGROTATE_PATH = Path('/etc/logrotate.d/rustproxy')
|
||||
SERVICE = 'rustproxy.service'
|
||||
GEOIP_SOURCES = {
|
||||
'user-country-ipv4.csv': 'https://github.com/sapics/ip-location-db/releases/download/latest/user-country-ipv4.csv',
|
||||
@@ -95,7 +96,7 @@ def digest(path: Path) -> str:
|
||||
def fetch_https(url: str, max_bytes: int) -> bytes:
|
||||
if urllib.parse.urlparse(url).scheme != 'https':
|
||||
raise ValueError('GeoIP downloads require HTTPS.')
|
||||
request = urllib.request.Request(url, headers={'User-Agent': 'RustProxy-installer/0.29.0'})
|
||||
request = urllib.request.Request(url, headers={'User-Agent': 'RustProxy-installer'})
|
||||
with urllib.request.urlopen(request, timeout=30) as response:
|
||||
if urllib.parse.urlparse(response.geturl()).scheme != 'https':
|
||||
raise ValueError('GeoIP download redirected away from HTTPS.')
|
||||
@@ -301,6 +302,24 @@ def install_shell_path(prefix: Path) -> None:
|
||||
write_private(SHELL_PATH_PROFILE, shell_path_profile(prefix), 0o644)
|
||||
|
||||
|
||||
def logrotate_config(user: str, group: str) -> bytes:
|
||||
# User/group are validated system account names, never interpolated from a request.
|
||||
for value in (user, group):
|
||||
if not re.fullmatch(r'[a-z_][a-z0-9_-]*\$?', value):
|
||||
raise ValueError('Unsafe service account for logrotate configuration.')
|
||||
return (f"# Managed by RustProxy installer; service account: {user}:{group}\n"
|
||||
'/var/log/rustproxy/*.jsonl /var/log/rustproxy/*.log /var/log/rustproxy/vhosts/*.jsonl /var/log/rustproxy/vhosts/*.log {\n'
|
||||
' daily\n maxsize 100M\n rotate 14\n missingok\n notifempty\n'
|
||||
' compress\n delaycompress\n copytruncate\n'
|
||||
f' su {user} {group}\n'
|
||||
'}\n').encode()
|
||||
|
||||
|
||||
def install_logrotate(record: dict) -> None:
|
||||
"""Keep logrotate in sync with the actual managed service account."""
|
||||
write_private(LOGROTATE_PATH, logrotate_config(record['user'], record['group']), 0o644)
|
||||
|
||||
|
||||
def check_shell_path(prefix: Path) -> bool:
|
||||
expected = shell_path_profile(prefix)
|
||||
try:
|
||||
|
||||
@@ -359,8 +359,8 @@ def update(record: dict, binary: Path, sha: str, start: bool, refresh_geoip: boo
|
||||
geoip_lock_fd: int | None = None
|
||||
geoip_stage: Path | None = None
|
||||
try:
|
||||
# Updates deliberately do not rewrite shell environment. Check the installer-
|
||||
# managed PATH profile early and report the exact remediation target instead.
|
||||
# Repair missing or stale installer-owned shell setup on upgrades.
|
||||
install_shell_path(prefix)
|
||||
check_shell_path(prefix)
|
||||
# Repair stale root:root/0700 metadata before staging/network work and before
|
||||
# stopping the currently healthy service.
|
||||
@@ -412,6 +412,8 @@ def update(record: dict, binary: Path, sha: str, start: bool, refresh_geoip: boo
|
||||
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
|
||||
install_shell_path(prefix)
|
||||
install_logrotate(replacement)
|
||||
print(f'Backup (contains private keys): {backup}')
|
||||
return replacement
|
||||
finally:
|
||||
@@ -443,6 +445,7 @@ def install(record: dict, binary: Path, sha: str, start: bool, install_geoip: bo
|
||||
record.update(release=release, version=version, sha256=sha)
|
||||
switch(prefix, release)
|
||||
install_shell_path(prefix)
|
||||
install_logrotate(record)
|
||||
# Record is written even if initial startup fails, so an update can repair it.
|
||||
write_private(prefix / 'install.json', json.dumps(record, indent=2).encode())
|
||||
write_private(UNIT_PATH, service_unit(record).encode(), 0o644)
|
||||
|
||||
@@ -8,6 +8,8 @@ from pathlib import Path
|
||||
import shutil
|
||||
import sys
|
||||
import uuid
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
||||
from scripts.version import project_version
|
||||
|
||||
MANAGED = ["build.rs", "langs", "deploy", "debian", "grafana", "make_zip.py", "src", "tests", "scripts", "tools", "integrations", "nagios", "docker-agent", "migrations", "examples", "docs", ".github", ".cargo",
|
||||
"Cargo.toml", "Cargo.lock", "rust-toolchain.toml", "dev.sh", "upgrade.sh", "install.sh", "update.sh",
|
||||
@@ -46,7 +48,7 @@ def upgrade(source: Path, target: Path, *, stopped: bool, data_dir: Path | None
|
||||
for name in MANAGED:
|
||||
if (target / name).is_symlink():
|
||||
raise ValueError(f"Managed target {name} is a symlink; refusing to replace it.")
|
||||
backup = target / ".upgrade-backup" / ("before-0.29.0-" + uuid.uuid4().hex[:12])
|
||||
backup = target / ".upgrade-backup" / ("before-" + project_version(target) + "-" + uuid.uuid4().hex[:12])
|
||||
backup.mkdir(parents=True, mode=0o700)
|
||||
installed: list[str] = []
|
||||
moved: list[str] = []
|
||||
@@ -84,7 +86,7 @@ def upgrade(source: Path, target: Path, *, stopped: bool, data_dir: Path | None
|
||||
remove_legacy = legacy.exists()
|
||||
if remove_legacy and (legacy.is_symlink() or not legacy.is_file()):
|
||||
raise ValueError("Obsolete renewal observation sidecar must be a regular file.")
|
||||
(backup / "upgrade.json").write_text(json.dumps({"from_directory":str(target), "to_version":"0.29.0", "managed_paths":MANAGED, "removed_obsolete_runtime_state": remove_legacy}, indent=2)+"\n", encoding="utf-8")
|
||||
(backup / "upgrade.json").write_text(json.dumps({"from_directory":str(target), "to_version":project_version(source), "managed_paths":MANAGED, "removed_obsolete_runtime_state": remove_legacy}, indent=2)+"\n", encoding="utf-8")
|
||||
if remove_legacy:
|
||||
legacy.unlink()
|
||||
except BaseException:
|
||||
@@ -115,7 +117,7 @@ def main() -> None:
|
||||
backup = upgrade(Path(__file__).resolve().parents[2], args.target, stopped=args.stopped, data_dir=data_dir)
|
||||
except (OSError, ValueError) as error:
|
||||
parser.exit(2, f"Upgrade refused or rolled back: {error}\n")
|
||||
print(f"Installed RustProxy 0.29.0. Previous managed source files: {backup}")
|
||||
print(f"Installed RustProxy {project_version(args.target)}. Previous managed source files: {backup}")
|
||||
print("Preserved data, .env, .git, target and other unmanaged files. No database migration was run.")
|
||||
print(f"Next: cd {args.target}; ./dev.sh test; ./dev.sh")
|
||||
|
||||
|
||||
Executable
+15
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Read the canonical RustProxy release version from Cargo.toml."""
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tomllib
|
||||
|
||||
|
||||
def project_version(root: Path | None = None) -> str:
|
||||
manifest = (root or Path(__file__).resolve().parents[1]) / "Cargo.toml"
|
||||
data = tomllib.loads(manifest.read_text(encoding="utf-8"))
|
||||
return data["package"]["version"]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(project_version(Path(sys.argv[1]) if len(sys.argv) > 1 else None))
|
||||
Reference in new issue
Block a user