104 lines
6.3 KiB
Python
104 lines
6.3 KiB
Python
"""Exercise a real previous installation inside a disposable rootless container.
|
|
Requires podman, the old tarball argument and the newly built amd64 release.
|
|
"""
|
|
import http.cookies
|
|
import json
|
|
from pathlib import Path
|
|
import secrets
|
|
import subprocess
|
|
import sys
|
|
import tarfile
|
|
import tempfile
|
|
import time
|
|
import urllib.request
|
|
|
|
project = Path(__file__).resolve().parent.parent
|
|
name = 'bluebird-upgrade-' + secrets.token_hex(4)
|
|
password = 'upgrade-preserved-password'
|
|
base = 'http://127.0.0.1:18996'
|
|
origin = 'https://files.example.test'
|
|
|
|
def run(*args, **kw):
|
|
return subprocess.run(args, check=True, capture_output=True, text=True, **kw).stdout
|
|
|
|
def inside(*args, **kw):
|
|
return run('podman', 'exec', name, *args, **kw)
|
|
|
|
with tempfile.TemporaryDirectory(prefix='bluebird-upgrade-old-') as old:
|
|
with tarfile.open(sys.argv[1]) as archive:
|
|
archive.extractall(old, filter='data')
|
|
try:
|
|
run('podman', 'run', '--rm', '-d', '--security-opt', 'label=disable', '--name', name,
|
|
'-p', '127.0.0.1:18996:18996', '-v', old+':/oldpkg:ro',
|
|
'-v', str(project/'build/linux-amd64')+':/pkg:ro',
|
|
'docker.io/library/debian:bookworm-slim', 'sleep', '300')
|
|
inside('env', 'BLUEBIRD_ADMIN_PASSWORD='+password, '/oldpkg/install.sh',
|
|
'--url', origin, '--listen', '0.0.0.0:18996', '--encrypt', '--no-start')
|
|
inside('sed','-i','s/min_free_bytes = 1073741824/min_free_bytes = 0/', '/opt/bluebird/config.toml')
|
|
key_before = inside('sha256sum', '/opt/bluebird/var/secrets/age.key')
|
|
cookies = {}
|
|
def call(method, path, body=None):
|
|
data = body if isinstance(body, bytes) else json.dumps(body).encode() if body is not None else None
|
|
req = urllib.request.Request(base+'/api/v1'+path, data=data, method=method, headers={
|
|
'Origin': origin, 'X-Bluebird-Origin': origin,
|
|
'Cookie': '; '.join(k+'='+v for k,v in cookies.items()),
|
|
'Content-Type': 'application/octet-stream' if isinstance(body,bytes) else 'application/json'})
|
|
with urllib.request.urlopen(req, timeout=10) as response:
|
|
for line in response.headers.get_all('Set-Cookie',[]):
|
|
parsed = http.cookies.SimpleCookie(); parsed.load(line)
|
|
cookies.update({k:v.value for k,v in parsed.items()})
|
|
result=response.read()
|
|
return result if path.endswith('/download') else json.loads(result)
|
|
def start():
|
|
subprocess.run(['podman','exec','-d',name,'runuser','-u','bluebird','--','/opt/bluebird/bluebird','serve','--config','/opt/bluebird/config.toml'],check=True)
|
|
for _ in range(50):
|
|
try:
|
|
if call('GET','/health')['ready']: return
|
|
except OSError: time.sleep(.1)
|
|
raise AssertionError('Container service not ready')
|
|
start()
|
|
call('GET','/policy')
|
|
parcel=call('POST','/uploads',{'files':[{'name':'preserved.txt','size':21}],'days':7,'downloads':5,'renew':True})
|
|
call('PUT',f"/uploads/{parcel['id']}/files/{parcel['files'][0]['id']}", b'old encrypted payload')
|
|
share=call('POST',f"/uploads/{parcel['id']}/publish",{})
|
|
call('POST','/admin/login',{'username':'admin','password':password})
|
|
before=inside('sha256sum','/opt/bluebird/config.toml','/opt/bluebird/bluebird')
|
|
# Invalid candidates must not stop a running service or change files.
|
|
bad=subprocess.run(['podman','exec',name,'/pkg/upgrade.sh','--access-url','http://bad.test/path','--no-start'],capture_output=True)
|
|
assert bad.returncode != 0
|
|
assert before == inside('sha256sum','/opt/bluebird/config.toml','/opt/bluebird/bluebird')
|
|
assert call('GET','/health')['ready']
|
|
# No pgrep dependency: /proc/comm identifies only the native service.
|
|
inside('sh','-c','for p in /proc/[0-9]*; do [ "$(cat "$p/comm" 2>/dev/null)" != bluebird ] || kill -TERM "${p##*/}"; done')
|
|
for _ in range(50):
|
|
try: call('GET','/health')
|
|
except OSError: break
|
|
time.sleep(.1)
|
|
# Simulate a new-version integrity-check failure after files were replaced.
|
|
inside('sh','-c','mkdir /failpkg; cp /pkg/bluebird /failpkg/bluebird.real; cp /pkg/ARCH /pkg/upgrade.sh /failpkg/')
|
|
wrapper='#!/bin/sh\nif [ "$1" = check ]; then exit 1; fi\nexec "$(dirname "$0")/bluebird.real" "$@"\n'
|
|
inside('sh','-c','printf "%s" "$1" > /failpkg/bluebird; chmod 755 /failpkg/bluebird','wrapper',wrapper)
|
|
failed=subprocess.run(['podman','exec',name,'/failpkg/upgrade.sh','--lan-url',base,'--listen','0.0.0.0:18996','--no-start'],capture_output=True)
|
|
assert failed.returncode != 0 and '已恢复旧程序和配置' in failed.stderr.decode()
|
|
assert before == inside('sha256sum','/opt/bluebird/config.toml','/opt/bluebird/bluebird')
|
|
result=inside('/pkg/upgrade.sh','--lan-url',base,'--listen','0.0.0.0:18996',
|
|
'--access-url','http://files.example.test','--no-start')
|
|
assert '升级完成' in result
|
|
assert key_before == inside('sha256sum','/opt/bluebird/var/secrets/age.key')
|
|
assert inside('stat','-c','%U:%G:%a','/opt/bluebird/config.toml').strip() == 'root:bluebird:640'
|
|
assert 'data.tar.gz' in inside('find','/opt/bluebird/backups','-type','f')
|
|
start();cookies.clear()
|
|
call('POST','/admin/login',{'username':'admin','password':password})
|
|
assert call('GET','/admin/stats')['objects'] == 1
|
|
policy=call('GET','/policy');assert set(policy['access_urls']) == {origin,base,'http://files.example.test'}
|
|
for entrance in [origin,base,'http://files.example.test']:
|
|
origin=entrance;cookies.clear()
|
|
call('POST','/pickup/resolve',{'code':share['code']})
|
|
assert call('GET',f"/pickup/files/{parcel['files'][0]['id']}/download") == b'old encrypted payload'
|
|
print('升级验证通过:旧管理员密码、加密文件、密钥及取件码保留;三入口下载、权限、备份、非法配置保护及检查失败回滚通过。')
|
|
except subprocess.CalledProcessError as error:
|
|
print(error.stdout, error.stderr, file=sys.stderr)
|
|
raise
|
|
finally:
|
|
subprocess.run(['podman','rm','-f',name],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
|