Files

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)