v0.27.3
This commit is contained in:
1 parent
e84df445c6
commit
e87a382a7e
109 files changed
+1092
-2925
No files matched your search
Executable
+80
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Install/update a local release binary on Linux/systemd. No builds."""
|
||||
from __future__ import annotations
|
||||
|
||||
from deploy_core import * # stable public helper surface used by release checks
|
||||
from deploy_runtime import * # stable install/update helper surface
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('mode', choices=['install', 'update'])
|
||||
parser.add_argument('--binary', type=Path, default=ROOT / 'target/release/rust-proxy')
|
||||
parser.add_argument('--prefix', default='/opt/rustproxy')
|
||||
parser.add_argument('--data-dir', default='/var/lib/rustproxy')
|
||||
parser.add_argument('--user', default='rustproxy')
|
||||
parser.add_argument('--http', default='0.0.0.0:80')
|
||||
tls = parser.add_mutually_exclusive_group()
|
||||
tls.add_argument('--https', default='0.0.0.0:443', help='TLS address (default: 0.0.0.0:443); first certificate is self-signed')
|
||||
tls.add_argument('--no-https', action='store_true', help='Disable the default 443 TLS listener')
|
||||
parser.add_argument('--admin', default='127.0.0.1:9090')
|
||||
parser.add_argument('--allow-remote-admin', action='store_true', help='Allow --admin on 0.0.0.0/:: instead of loopback')
|
||||
parser.add_argument('--sha256', help='Expected SHA-256 of the supplied binary')
|
||||
parser.add_argument('--no-start', action='store_true', help='Leave the service stopped after install/update')
|
||||
parser.add_argument('--plan', action='store_true', help='Print a plan without root privileges, builds or host changes')
|
||||
parser.add_argument('--skip-geoip', action='store_true', help='Skip GeoIP install/update verification (country allow lists may use stale/missing data)')
|
||||
parser.add_argument('--reset-config', action='store_true', help='UPDATE ONLY, development recovery: discard saved vhosts/settings after backup while preserving account/certificates/private files')
|
||||
args = parser.parse_args()
|
||||
os.umask(0o077)
|
||||
try:
|
||||
prefix, data = safe_path(args.prefix), safe_path(args.data_dir)
|
||||
if args.reset_config and args.mode != 'update':
|
||||
raise ValueError('--reset-config is only valid with update.sh.')
|
||||
if prefix == data or prefix in data.parents or data in prefix.parents:
|
||||
raise ValueError('Code and private data must use separate non-nested directories.')
|
||||
validate_user(args.user)
|
||||
admin, remote_admin = admin_address(args.admin, args.allow_remote_admin)
|
||||
record = dict(schema=1, prefix=str(prefix), data_dir=str(data), user=args.user, http=address(args.http),
|
||||
https=None if args.no_https else address(args.https), admin=admin, remote_admin=remote_admin)
|
||||
ports = [x.rsplit(':', 1)[1] for x in (record['http'], record['https'], record['admin']) if x]
|
||||
if len(set(ports)) != len(ports):
|
||||
raise ValueError('HTTP, TLS and administration ports must differ.')
|
||||
binary = args.binary.absolute()
|
||||
if binary.is_symlink() or not binary.is_file() or not os.access(binary, os.X_OK):
|
||||
raise ValueError('Build the release binary as an ordinary user first (bash dev.sh release); provide --binary.')
|
||||
sha = digest(binary)
|
||||
if args.sha256 and args.sha256.lower() != sha:
|
||||
raise ValueError('Binary SHA-256 mismatch; no changes made.')
|
||||
if args.plan:
|
||||
print(json.dumps({'operation':args.mode, 'binary':str(binary), 'sha256':sha, 'install_defaults':record,
|
||||
'update_uses_saved_settings':args.mode == 'update', 'backup_before_update':True, 'rollback_code_and_data':True,
|
||||
'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',
|
||||
'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.')
|
||||
prefix.mkdir(parents=True, mode=0o755, exist_ok=True)
|
||||
if prefix.stat().st_uid != 0 or prefix.stat().st_mode & 0o022:
|
||||
raise ValueError('Installation prefix must be root-owned and not group/world-writable.')
|
||||
fd = os.open(prefix / '.deploy.lock', os.O_WRONLY | os.O_CREAT | os.O_NOFOLLOW, 0o600)
|
||||
with os.fdopen(fd, 'w') as lock:
|
||||
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
result = install(record, binary, sha, not args.no_start, not args.skip_geoip) if args.mode == 'install' else update(
|
||||
read_record(prefix), binary, sha, not args.no_start, not args.skip_geoip, args.reset_config
|
||||
)
|
||||
print(f'RustProxy {result["version"]} installed. Administration: http://{result["admin"]}')
|
||||
print(f'Generate setup token: sudo -u {result["user"]} {prefix}/current/rust-proxy --data-dir {result["data_dir"]} admin setup-token')
|
||||
print(f'Initialize account: sudo -u {result["user"]} {prefix}/current/rust-proxy --data-dir {result["data_dir"]} admin init')
|
||||
if result.get('remote_admin'):
|
||||
print('WARNING: remote administration is plain HTTP. Restrict it with a firewall/private network; a protected tunnel remains safer.')
|
||||
else:
|
||||
print('Use an SSH tunnel for remote administration.')
|
||||
print('No demo backend or firewall changes were installed.')
|
||||
except (OSError, ValueError, RuntimeError, subprocess.SubprocessError, KeyError) as error:
|
||||
parser.exit(1, f'Deployment failed: {error}\n')
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in new issue
Block a user