mirror of
https://gitlab.com/fdroid/fdroidserver.git
synced 2024-11-19 21:30:10 +01:00
b4d25d2834
Years ago, vagrant-libvirt did not implement the `vagrant package` command that we needed, and there were no Ruby people around to implement it for us. So we hacked a custom version in our Python wrapper. Now, vagrant-libvirt v0.7.0 does implement it, so this switches things to just using `vagrant package`
467 lines
20 KiB
Python
Executable File
467 lines
20 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
import os
|
|
import re
|
|
import requests
|
|
import stat
|
|
import sys
|
|
import shutil
|
|
import subprocess
|
|
import vagrant
|
|
import hashlib
|
|
import yaml
|
|
import json
|
|
import logging
|
|
from clint.textui import progress
|
|
from optparse import OptionParser
|
|
import fdroidserver.tail
|
|
import fdroidserver.vmtools
|
|
|
|
|
|
parser = OptionParser()
|
|
parser.add_option('-v', '--verbose', action="count", dest='verbosity', default=1,
|
|
help="Spew out even more information than normal")
|
|
parser.add_option('-q', action='store_const', const=0, dest='verbosity')
|
|
parser.add_option("-c", "--clean", action="store_true", default=False,
|
|
help="Build from scratch, rather than attempting to update the existing server")
|
|
parser.add_option('--skip-cache-update', action="store_true", default=False,
|
|
help="""Skip downloading and checking cache."""
|
|
"""This assumes that the cache is already downloaded completely.""")
|
|
parser.add_option('--keep-box-file', action="store_true", default=False,
|
|
help="""Box file will not be deleted after adding it to box storage"""
|
|
""" (KVM-only).""")
|
|
options, args = parser.parse_args()
|
|
|
|
|
|
logformat = '%(levelname)s: %(message)s'
|
|
loglevel = logging.DEBUG
|
|
if options.verbosity == 1:
|
|
loglevel = logging.INFO
|
|
elif options.verbosity <= 0:
|
|
loglevel = logging.WARNING
|
|
logging.basicConfig(format=logformat, level=loglevel)
|
|
|
|
tail = None
|
|
|
|
BASEBOX_DEFAULT = 'fdroid/bullseye64'
|
|
BASEBOX_VERSION_DEFAULT = "11.20221010.1"
|
|
BASEBOX_CHECKSUMS = {
|
|
"11.20221010.1": {
|
|
"libvirt": {
|
|
"box.img": "c2114aa276c176fa65b8072f5dcd1e8a6ab9f7d15fd5da791727a0164fd43254",
|
|
"Vagrantfile": "f9c6fcbb47a4d0d33eb066859c8e87efd642287a638bd7da69a9e7a6f25fec47",
|
|
"metadata.json": "42b96a01106c25f3a222ddad0baead0b811cc64926f924fb836bbfa43580e646",
|
|
},
|
|
"virtualbox": {
|
|
"box.ovf": "5e4de5f1f4b481b2c1917c0b2f6e6334f4741cc18c5b278e3bafb094535ff2cb",
|
|
"box.vmdk": "737053bc886037ae76bb38a1776eba2a5579d49423de990e93ef4a3f0cab4f1c",
|
|
"Vagrantfile": "0bbc2ae97668d8da27ab97b766752dcd0bf9e41900e21057de15a58ee7fae47d",
|
|
"metadata.json": "ffdaa989f2f6932cd8042e1102371f405cc7ad38e324210a1326192e4689e83a",
|
|
}
|
|
},
|
|
'11.20220317.1': {
|
|
'libvirt': {
|
|
'box.img': 'fbde152a2f61d191983be9d1dbeae2591af32cca1ec27daa342485d97187515e',
|
|
'metadata.json': '42b96a01106c25f3a222ddad0baead0b811cc64926f924fb836bbfa43580e646',
|
|
'Vagrantfile': 'f9c6fcbb47a4d0d33eb066859c8e87efd642287a638bd7da69a9e7a6f25fec47',
|
|
},
|
|
'virtualbox': {
|
|
'box.ovf': 'becd5cea2666d42e12def13a91766aa0d4b0e8e6f53102486c2a6cdb4e401b08',
|
|
'box.vmdk': '49c96a58a3ee99681d348075864a290c60a8d334fddd21be453c825fcee75eda',
|
|
'metadata.json': 'ffdaa989f2f6932cd8042e1102371f405cc7ad38e324210a1326192e4689e83a',
|
|
'Vagrantfile': '0bbc2ae97668d8da27ab97b766752dcd0bf9e41900e21057de15a58ee7fae47d',
|
|
}
|
|
},
|
|
}
|
|
|
|
config = {
|
|
'basebox': BASEBOX_DEFAULT,
|
|
'debian_mirror': 'http://deb.debian.org/debian/',
|
|
'apt_package_cache': False,
|
|
'copy_caches_from_host': False,
|
|
'boot_timeout': 600,
|
|
'cachedir': os.path.join(os.getenv('HOME'), '.cache', 'fdroidserver'),
|
|
'cpus': 1,
|
|
'memory': 2048,
|
|
'hwvirtex': 'off',
|
|
'vm_provider': 'virtualbox',
|
|
}
|
|
|
|
# load config file, if present
|
|
if os.path.exists('makebuildserver.config.py'):
|
|
exec(compile(open('makebuildserver.config.py').read(), 'makebuildserver.config.py', 'exec'), config)
|
|
elif os.path.exists('makebs.config.py'):
|
|
# this is the old name for the config file
|
|
exec(compile(open('makebs.config.py').read(), 'makebs.config.py', 'exec'), config)
|
|
if '__builtins__' in config:
|
|
del config['__builtins__'] # added by compile/exec
|
|
logging.debug("makebuildserver.config.py parsed -> %s", json.dumps(config, indent=4, sort_keys=True))
|
|
if config['basebox'] == BASEBOX_DEFAULT and 'basebox_version' not in config:
|
|
config['basebox_version'] = BASEBOX_VERSION_DEFAULT
|
|
# note: vagrant allows putting '/' into the name of a local box,
|
|
# so this check is not completely reliable, but better than nothing
|
|
if 'basebox_version' in config and 'basebox' in config and '/' not in config['basebox']:
|
|
logging.critical("Can not get version '{version}' for basebox '{box}', "
|
|
"vagrant does not support versioning for locally added boxes."
|
|
.format(box=config['basebox'], version=config['basebox_version']))
|
|
|
|
# Update cached files.
|
|
if not os.path.exists(config['cachedir']):
|
|
os.makedirs(config['cachedir'], 0o755)
|
|
logging.debug('created cachedir {} because it did not exists.'.format(config['cachedir']))
|
|
|
|
if config['vm_provider'] == 'libvirt':
|
|
tmp = config['cachedir']
|
|
while tmp != '/':
|
|
mode = os.stat(tmp).st_mode
|
|
if not (stat.S_IXUSR & mode and stat.S_IXGRP & mode and stat.S_IXOTH & mode):
|
|
logging.critical('ERROR: %s will not be accessible to the VM! To fix, run:', tmp)
|
|
logging.critical(' chmod a+X %s', tmp)
|
|
sys.exit(1)
|
|
tmp = os.path.dirname(tmp)
|
|
logging.debug('cache dir %s is accessible for libvirt vm.', config['cachedir'])
|
|
|
|
if config['apt_package_cache']:
|
|
config['aptcachedir'] = config['cachedir'] + '/apt/archives'
|
|
logging.debug('aptcachedir is set to %s', config['aptcachedir'])
|
|
aptcachelock = os.path.join(config['aptcachedir'], 'lock')
|
|
if os.path.isfile(aptcachelock):
|
|
logging.info('apt cache dir is locked, removing lock')
|
|
os.remove(aptcachelock)
|
|
aptcachepartial = os.path.join(config['aptcachedir'], 'partial')
|
|
if os.path.isdir(aptcachepartial):
|
|
logging.info('removing partial downloads from apt cache dir')
|
|
shutil.rmtree(aptcachepartial)
|
|
|
|
CACHE_FILES = [
|
|
('https://services.gradle.org/distributions/gradle-6.8.3-bin.zip',
|
|
'7faa7198769f872826c8ef4f1450f839ec27f0b4d5d1e51bade63667cbccd205'),
|
|
('https://services.gradle.org/distributions/gradle-7.0.2-bin.zip',
|
|
'0e46229820205440b48a5501122002842b82886e76af35f0f3a069243dca4b3c'),
|
|
('https://dl.google.com/android/repository/android-ndk-r23c-linux.zip',
|
|
'6ce94604b77d28113ecd588d425363624a5228d9662450c48d2e4053f8039242'),
|
|
]
|
|
|
|
|
|
def sha256_for_file(path):
|
|
with open(path, 'rb') as f:
|
|
s = hashlib.sha256()
|
|
while True:
|
|
data = f.read(4096)
|
|
if not data:
|
|
break
|
|
s.update(data)
|
|
return s.hexdigest()
|
|
|
|
|
|
def verify_file_sha256(path, sha256):
|
|
if sha256_for_file(path) != sha256:
|
|
logging.critical("File verification for '{path}' failed! "
|
|
"expected sha256 checksum: {checksum}"
|
|
.format(path=path, checksum=sha256))
|
|
sys.exit(1)
|
|
else:
|
|
logging.debug("sucessfully verifyed file '{path}' "
|
|
"('{checksum}')".format(path=path,
|
|
checksum=sha256))
|
|
|
|
|
|
def get_vagrant_home():
|
|
return os.environ.get('VAGRANT_HOME',
|
|
os.path.join(os.path.expanduser('~'),
|
|
'.vagrant.d'))
|
|
|
|
|
|
def run_via_vagrant_ssh(v, cmdlist):
|
|
if (isinstance(cmdlist, str) or isinstance(cmdlist, bytes)):
|
|
cmd = cmdlist
|
|
else:
|
|
cmd = ' '.join(cmdlist)
|
|
v._run_vagrant_command(['ssh', '-c', cmd])
|
|
|
|
|
|
def update_cache(cachedir):
|
|
count_files = 0
|
|
for srcurl, shasum in CACHE_FILES:
|
|
filename = os.path.basename(srcurl)
|
|
local_filename = os.path.join(cachedir, filename)
|
|
count_files = count_files + 1
|
|
if os.path.exists(local_filename):
|
|
if sha256_for_file(local_filename) == shasum:
|
|
logging.info("\t...shasum verified for '{filename}'\t({filecounter} of {filesum} files)".format(filename=local_filename, filecounter=count_files, filesum=len(CACHE_FILES)))
|
|
continue
|
|
local_length = os.path.getsize(local_filename)
|
|
else:
|
|
local_length = -1
|
|
|
|
resume_header = {}
|
|
download = True
|
|
|
|
try:
|
|
r = requests.head(srcurl, allow_redirects=True, timeout=60)
|
|
if r.status_code == 200:
|
|
content_length = int(r.headers.get('content-length'))
|
|
else:
|
|
content_length = local_length # skip the download
|
|
except requests.exceptions.RequestException as e:
|
|
content_length = local_length # skip the download
|
|
logging.warn('%s', e)
|
|
|
|
if local_length == content_length:
|
|
download = False
|
|
elif local_length > content_length:
|
|
logging.info('deleting corrupt file from cache: %s', local_filename)
|
|
os.remove(local_filename)
|
|
logging.info("Downloading %s to cache", filename)
|
|
elif local_length > -1 and local_length < content_length:
|
|
logging.info("Resuming download of %s", local_filename)
|
|
resume_header = {'Range': 'bytes=%d-%d' % (local_length, content_length)}
|
|
else:
|
|
logging.info("Downloading %s to cache", filename)
|
|
|
|
if download:
|
|
r = requests.get(srcurl, headers=resume_header, stream=True,
|
|
allow_redirects=True, timeout=60)
|
|
content_length = int(r.headers.get('content-length'))
|
|
with open(local_filename, 'ab') as f:
|
|
for chunk in progress.bar(r.iter_content(chunk_size=65536),
|
|
expected_size=(content_length / 65536) + 1):
|
|
if chunk: # filter out keep-alive new chunks
|
|
f.write(chunk)
|
|
|
|
v = sha256_for_file(local_filename)
|
|
if v == shasum:
|
|
logging.info("\t...shasum verified for '{filename}'\t({filecounter} of {filesum} files)".format(filename=local_filename, filecounter=count_files, filesum=len(CACHE_FILES)))
|
|
else:
|
|
logging.critical("Invalid shasum of '%s' detected for %s", v, local_filename)
|
|
os.remove(local_filename)
|
|
sys.exit(1)
|
|
|
|
|
|
def debug_log_vagrant_vm(vm_dir, config):
|
|
if options.verbosity >= 3:
|
|
_vagrant_dir = os.path.join(vm_dir, '.vagrant')
|
|
logging.debug('check %s dir exists? -> %r', _vagrant_dir, os.path.isdir(_vagrant_dir))
|
|
logging.debug('> vagrant status')
|
|
subprocess.call(['vagrant', 'status'], cwd=vm_dir)
|
|
logging.debug('> vagrant box list')
|
|
subprocess.call(['vagrant', 'box', 'list'])
|
|
if config['vm_provider'] == 'libvirt':
|
|
logging.debug('> virsh -c qmeu:///system list --all')
|
|
subprocess.call(['virsh', '-c', 'qemu:///system', 'list', '--all'])
|
|
domain = 'buildserver_default'
|
|
logging.debug('> virsh -c qemu:///system snapshot-list %s', domain)
|
|
subprocess.call(['virsh', '-c', 'qemu:///system', 'snapshot-list', domain])
|
|
|
|
|
|
def main():
|
|
global config, tail
|
|
|
|
if options.skip_cache_update:
|
|
logging.info('skipping cache update and verification...')
|
|
else:
|
|
update_cache(config['cachedir'])
|
|
|
|
# use VirtualBox software virtualization if hardware is not available,
|
|
# like if this is being run in kvm or some other VM platform, like
|
|
# http://jenkins.debian.net, the values are 'on' or 'off'
|
|
if sys.platform.startswith('darwin'):
|
|
# all < 10 year old Macs work, and OSX servers as VM host are very
|
|
# rare, but this could also be auto-detected if someone codes it
|
|
config['hwvirtex'] = 'on'
|
|
logging.info('platform is darwnin -> hwvirtex = \'on\'')
|
|
elif os.path.exists('/proc/cpuinfo'):
|
|
with open('/proc/cpuinfo') as f:
|
|
contents = f.read()
|
|
if 'vmx' in contents or 'svm' in contents:
|
|
config['hwvirtex'] = 'on'
|
|
logging.info('found \'vmx\' or \'svm\' in /proc/cpuinfo -> hwvirtex = \'on\'')
|
|
|
|
serverdir = os.path.join(os.getcwd(), 'buildserver')
|
|
logfilename = os.path.join(serverdir, 'up.log')
|
|
if not os.path.exists(logfilename):
|
|
open(logfilename, 'a').close() # create blank file
|
|
log_cm = vagrant.make_file_cm(logfilename)
|
|
v = vagrant.Vagrant(root=serverdir, out_cm=log_cm, err_cm=log_cm)
|
|
# https://phoenhex.re/2018-03-25/not-a-vagrant-bug
|
|
os_env = os.environ.copy()
|
|
os_env['VAGRANT_DISABLE_VBOXSYMLINKCREATE'] = '1'
|
|
v.env = os_env
|
|
|
|
if options.verbosity >= 2:
|
|
tail = fdroidserver.tail.Tail(logfilename)
|
|
tail.start()
|
|
|
|
vm = fdroidserver.vmtools.get_build_vm(serverdir, provider=config['vm_provider'])
|
|
if options.clean:
|
|
vm.destroy()
|
|
|
|
# Check against the existing Vagrantfile.yaml, and if they differ, we
|
|
# need to create a new box:
|
|
vf = os.path.join(serverdir, 'Vagrantfile.yaml')
|
|
writevf = True
|
|
if os.path.exists(vf):
|
|
logging.info('Halting %s', serverdir)
|
|
v.halt()
|
|
with open(vf, 'r', encoding='utf-8') as f:
|
|
oldconfig = yaml.safe_load(f)
|
|
if config != oldconfig:
|
|
logging.info("Server configuration has changed, rebuild from scratch is required")
|
|
vm.destroy()
|
|
else:
|
|
logging.info("Re-provisioning existing server")
|
|
writevf = False
|
|
else:
|
|
logging.info("No existing server - building from scratch")
|
|
if writevf:
|
|
with open(vf, 'w', encoding='utf-8') as f:
|
|
yaml.safe_dump(config, f)
|
|
|
|
# Check if selected provider is supported
|
|
if config['vm_provider'] not in ['libvirt', 'virtualbox']:
|
|
logging.critical("Currently selected VM provider '{vm_provider}' "
|
|
"is not supported. (please choose from: "
|
|
"virtualbox, libvirt)"
|
|
.format(vm_provider=config['cm_provider']))
|
|
sys.exit(1)
|
|
# Check if selected Vagrant box is available
|
|
available_boxes_by_provider = [x.name for x in v.box_list() if x.provider == config['vm_provider']]
|
|
if '/' not in config['basebox'] and config['basebox'] not in available_boxes_by_provider:
|
|
logging.critical("Vagrant box '{basebox}' not available "
|
|
"for '{vm_provider}' VM provider. "
|
|
"Please make sure it's added to vagrant. "
|
|
"(If you need a basebox to begin with, "
|
|
"here is how we're bootstrapping it: "
|
|
"https://gitlab.com/fdroid/basebox)"
|
|
.format(vm_provider=config['vm_provider'],
|
|
basebox=config['basebox']))
|
|
sys.exit(1)
|
|
|
|
# Download and verify pre-built Vagrant boxes
|
|
if config['basebox'] == BASEBOX_DEFAULT:
|
|
buildserver_not_created = any([True for x in v.status() if x.state == 'not_created' and x.name == 'default'])
|
|
if buildserver_not_created or options.clean:
|
|
# make vagrant download and add basebox
|
|
target_basebox_installed = any([x for x in v.box_list() if x.name == BASEBOX_DEFAULT and x.provider == config['vm_provider'] and x.version == config['basebox_version']])
|
|
if not target_basebox_installed:
|
|
cmd = [shutil.which('vagrant'), 'box', 'add', BASEBOX_DEFAULT,
|
|
'--box-version=' + config['basebox_version'],
|
|
'--provider=' + config['vm_provider']]
|
|
ret_val = subprocess.call(cmd)
|
|
if ret_val != 0:
|
|
logging.critical("downloading basebox '{box}' "
|
|
"({provider}, version {version}) failed."
|
|
.format(box=config['basebox'],
|
|
provider=config['vm_provider'],
|
|
version=config['basebox_version']))
|
|
sys.exit(1)
|
|
# verify box
|
|
if config['basebox_version'] not in BASEBOX_CHECKSUMS.keys():
|
|
logging.critical("can not verify '{box}', "
|
|
"unknown basebox version '{version}'"
|
|
.format(box=config['basebox'],
|
|
version=config['basebox_version']))
|
|
sys.exit(1)
|
|
for filename, sha256 in BASEBOX_CHECKSUMS[config['basebox_version']][config['vm_provider']].items():
|
|
verify_file_sha256(os.path.join(get_vagrant_home(),
|
|
'boxes',
|
|
BASEBOX_DEFAULT.replace('/', '-VAGRANTSLASH-'),
|
|
config['basebox_version'],
|
|
config['vm_provider'],
|
|
filename),
|
|
sha256)
|
|
logging.info("successfully verified: '{box}' "
|
|
"({provider}, version {version})"
|
|
.format(box=config['basebox'],
|
|
provider=config['vm_provider'],
|
|
version=config['basebox_version']))
|
|
else:
|
|
logging.debug('not updating basebox ...')
|
|
else:
|
|
logging.debug('using unverified basebox ...')
|
|
|
|
logging.info("Configuring build server VM")
|
|
debug_log_vagrant_vm(serverdir, config)
|
|
try:
|
|
v.up(provision=True)
|
|
except subprocess.CalledProcessError:
|
|
debug_log_vagrant_vm(serverdir, config)
|
|
logging.error("'vagrant up' failed.")
|
|
sys.exit(1)
|
|
|
|
if config['copy_caches_from_host']:
|
|
ssh_config = v.ssh_config()
|
|
user = re.search(r'User ([^ \n]+)', ssh_config).group(1)
|
|
hostname = re.search(r'HostName ([^ \n]+)', ssh_config).group(1)
|
|
port = re.search(r'Port ([0-9]+)', ssh_config).group(1)
|
|
key = re.search(r'IdentityFile ([^ \n]+)', ssh_config).group(1)
|
|
|
|
for d in ('.m2', '.gradle/caches', '.gradle/wrapper', '.pip_download_cache'):
|
|
fullpath = os.path.join(os.getenv('HOME'), d)
|
|
os.system('date')
|
|
print('rsyncing', fullpath, 'into VM')
|
|
if os.path.isdir(fullpath):
|
|
ssh_command = ' '.join(('ssh -i {0} -p {1}'.format(key, port),
|
|
'-o StrictHostKeyChecking=no',
|
|
'-o UserKnownHostsFile=/dev/null',
|
|
'-o LogLevel=FATAL',
|
|
'-o IdentitiesOnly=yes',
|
|
'-o PasswordAuthentication=no'))
|
|
# TODO vagrant 1.5+ provides `vagrant rsync`
|
|
run_via_vagrant_ssh(v, ['cd ~ && test -d', d, '|| mkdir -p', d])
|
|
subprocess.call(['rsync', '-ax', '--delete', '-e',
|
|
ssh_command,
|
|
fullpath + '/',
|
|
user + '@' + hostname + ':~/' + d + '/'])
|
|
|
|
# this file changes every time but should not be cached
|
|
run_via_vagrant_ssh(v, ['rm', '-f', '~/.gradle/caches/modules-2/modules-2.lock'])
|
|
run_via_vagrant_ssh(v, ['rm', '-fr', '~/.gradle/caches/*/plugin-resolution/'])
|
|
|
|
logging.info("Stopping build server VM")
|
|
v.halt()
|
|
|
|
logging.info("Packaging")
|
|
boxfile = os.path.join(os.getcwd(), 'buildserver.box')
|
|
if os.path.exists(boxfile):
|
|
os.remove(boxfile)
|
|
|
|
v.package(output=boxfile)
|
|
|
|
logging.info("Adding box")
|
|
vm.box_add('buildserver', boxfile, force=True)
|
|
|
|
if 'buildserver' not in subprocess.check_output(['vagrant', 'box', 'list']).decode('utf-8'):
|
|
logging.critical('could not add box \'%s\' as \'buildserver\', terminating', boxfile)
|
|
sys.exit(1)
|
|
|
|
if not options.keep_box_file:
|
|
logging.debug("""box added to vagrant, removing generated box file '%s'""",
|
|
boxfile)
|
|
os.remove(boxfile)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
if not os.path.exists('makebuildserver') and not os.path.exists('buildserver'):
|
|
logging.critical('This must be run as ./makebuildserver in fdroidserver.git!')
|
|
sys.exit(1)
|
|
|
|
if os.path.isfile('/usr/bin/systemd-detect-virt'):
|
|
try:
|
|
virt = subprocess.check_output('/usr/bin/systemd-detect-virt').strip().decode('utf-8')
|
|
except subprocess.CalledProcessError:
|
|
virt = 'none'
|
|
if virt in ('qemu', 'kvm', 'bochs'):
|
|
logging.info('Running in a VM guest, defaulting to QEMU/KVM via libvirt')
|
|
config['vm_provider'] = 'libvirt'
|
|
elif virt != 'none':
|
|
logging.info('Running in an unsupported VM guest (%s)!', virt)
|
|
logging.debug('detected virt: %s', virt)
|
|
|
|
try:
|
|
main()
|
|
finally:
|
|
if tail is not None:
|
|
tail.stop()
|