Merge "Debian: pkgbuilder: Support parallel build with multiple chroots"

This commit is contained in:
Zuul 2022-08-09 15:27:54 +00:00 committed by Gerrit Code Review
commit 416962d788
5 changed files with 618 additions and 206 deletions

View File

@ -53,6 +53,7 @@ RUN groupadd crontab
COPY stx/toCOPY/pkgbuilder/app.py /opt/
COPY stx/toCOPY/pkgbuilder/debbuilder.py /opt/
COPY stx/toCOPY/pkgbuilder/schrootspool.py /opt/
COPY stx/toCOPY/pkgbuilder/setup.sh /opt/
COPY stx/toCOPY/pkgbuilder/debbuilder.conf /etc/sbuild/sbuild.conf

View File

@ -11,7 +11,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Copyright (C) 2021 Wind River Systems,Inc
# Copyright (C) 2021-2022 Wind River Systems,Inc
#
from debbuilder import Debbuilder
from flask import Flask
@ -19,142 +19,124 @@ from flask import jsonify
from flask import request
import logging
STX_DISTRO = 'bullseye'
STX_ARCH = 'amd64'
PKG_BUILDER_LOG = '/localdisk/pkgbuilder.log'
app = Flask(__name__)
dbuilder = None
app.debug = True
logging.basicConfig(level=logging.DEBUG)
log = logging.getLogger('pkgbuilder')
handler = logging.FileHandler(PKG_BUILDER_LOG, encoding='UTF-8')
log_format = logging.Formatter("%(asctime)s - %(levelname)s: %(message)s")
handler.setFormatter(log_format)
log.addHandler(handler)
dbuilder = Debbuilder('private', STX_DISTRO, STX_ARCH, log)
response = {}
def dbuider_initialized():
global dbuilder
if not dbuilder:
response['status'] = 'fail'
response['msg'] = 'Package builder is not initialized'
return False
return True
def log_request(action, request):
"""
Print request with parameters
"""
msg = 'Received request: ' + action + ': {'
for key in request:
value = key + ':' + request[key]
msg = ' '.join([msg, value])
msg = msg + '}'
log.info(msg)
@app.route('/pkgbuilder/state', methods=['GET'])
def get_state():
response = {
'status': dbuilder.state,
'msg': ''
}
if dbuider_initialized():
response = dbuilder.state()
return jsonify(response)
@app.route('/pkgbuilder/loadchroot', methods=['GET'])
def load_chroot():
attrs = ['user', 'project']
if all(t in request.form for t in attrs):
user = request.form['user']
project = request.form['project']
response = dbuilder.load_chroot(user, project)
log.info("Reply to load chroot, response=%s", str(response))
else:
response = {
'status': 'fail',
'msg': 'invalid request, missing parameter'
}
log_request('loadchroot', request.form)
if dbuider_initialized():
response = dbuilder.load_chroot(request.form)
return jsonify(response)
@app.route('/pkgbuilder/clonechroot', methods=['GET'])
def clone_chroot():
log_request('clonechroot', request.form)
if dbuider_initialized():
response = dbuilder.clone_chroot(request.form)
return jsonify(response)
@app.route('/pkgbuilder/savechroot', methods=['GET'])
def save_chroot():
attrs = ['user', 'project']
if all(t in request.form for t in attrs):
user = request.form['user']
project = request.form['project']
response = dbuilder.save_chroot(user, project)
log.info("Reply to save chroot, response=%s", str(response))
else:
response = {
'status': 'fail',
'msg': 'invalid request, missing parameter'
}
log_request('savechroot', request.form)
if dbuider_initialized():
response = dbuilder.save_chroot(request.form)
return jsonify(response)
@app.route('/pkgbuilder/addchroot', methods=['GET'])
def add_chroot():
if not request.form or 'user' not in request.form:
log.error("Invalid request to add user chroot")
response = {
'status': 'fail',
'msg': 'invalid request'
}
else:
user = request.form['user']
project = request.form['project']
if 'mirror' in request.form:
response = dbuilder.add_chroot(user, project,
request.form['mirror'])
else:
response = dbuilder.add_chroot(user, project)
log.info("Reply to add user chroot, response=%s", str(response))
log_request('addchroot', request.form)
if dbuider_initialized():
response = dbuilder.add_chroot(request.form)
return jsonify(response)
@app.route('/pkgbuilder/addtask', methods=['GET'])
@app.route('/pkgbuilder/refreshchroots', methods=['GET'])
def refresh_chroot():
log_request('refreshchroots', request.form)
if dbuider_initialized():
response = dbuilder.refresh_chroots(request.form)
return jsonify(response)
@app.route('/pkgbuilder/addtask', methods=['POST'])
def add_task():
response = {}
attrs = ['user', 'project', 'dsc', 'type', 'name', 'mode', 'run_tests']
if not all(t in request.form for t in attrs):
log.error("Invalid request to add task")
response['status'] = 'fail'
response['msg'] = 'invalid request'
else:
dbuilder.mode = request.form['mode']
user = request.form['user']
project = request.form['project']
task_info = {
'package': request.form['name'],
'dsc': request.form['dsc'],
'type': request.form['type'],
'run_tests': request.form['run_tests']
}
if 'jobs' in request.form:
task_info['jobs'] = request.form['jobs']
response = dbuilder.add_task(user, project, task_info)
log.info("Reply to add task, response=%s", str(response))
if dbuider_initialized():
reqs = request.get_json()
log.debug("Request for adding task: %s", reqs)
response = dbuilder.add_task(reqs)
return jsonify(response)
@app.route('/pkgbuilder/killtask', methods=['GET'])
def clean_env():
response = {}
attrs = ['user', 'owner']
if all(t in request.form for t in attrs):
user = request.form['user']
owner = request.form['owner']
response = dbuilder.kill_task(user, owner)
log.info("Reply to kill task, response=%s", str(response))
else:
log.error("Invalid request to kill task")
response = {
'status': 'fail',
'msg': 'invalid request'
}
log_request('killtask', request.form)
if dbuider_initialized():
response = dbuilder.kill_task(request.form)
return jsonify(response)
@app.route('/pkgbuilder/stoptask', methods=['GET'])
def stop_task():
response = {}
attrs = ['user']
if all(t in request.form for t in attrs):
user = request.form['user']
response = dbuilder.stop_task(user)
log.info("Reply to stop task, response=%s", str(response))
else:
log.error("Invalid request to stop task")
response = {
'status': 'fail',
'msg': 'invalid request'
}
log_request('stoptask', request.form)
if dbuider_initialized():
response = dbuilder.stop_task(request.form)
return jsonify(response)
@app.route('/pkgbuilder/cleanstamp', methods=['GET'])
def clean_stamp():
log_request('cleanstamp', request.form)
if dbuider_initialized():
response = dbuilder.clean_stamp(request.form)
return jsonify(response)
if __name__ == '__main__':
app.debug = True
handler = logging.FileHandler('/localdisk/pkgbuilder.log',
encoding='UTF-8')
logging.basicConfig(level=logging.DEBUG)
log_format = logging.Formatter('pkgbuilder: %(levelname)s %(message)s')
handler.setFormatter(log_format)
log = logging.getLogger('pkgbuilder')
log.addHandler(handler)
dbuilder = Debbuilder('private', log)
app.run(host='0.0.0.0', port=80, debug=True)

View File

@ -24,8 +24,7 @@ $lintian_require_success = 0;
$run_piuparts = 0;
$purge_build_deps = 'always';
$purge_build_directory = 'always';
$extra_repositories = ['deb [trusted=yes] http://stx-stx-repomgr:80/deb-local-build @DEBIAN_DISTRIBUTION@ main',
'deb [trusted=yes] http://stx-stx-repomgr:80/deb-local-binary @DEBIAN_DISTRIBUTION@ main'];
$extra_repositories = ['deb [trusted=yes] http://stx-stx-repomgr:80/deb-local-binary @DEBIAN_DISTRIBUTION@ main'];
$log_colour = 1;
$build_environment = {
'OSTREE_OSNAME' => '@OSTREE_OSNAME@'
@ -36,7 +35,6 @@ $external_commands = {
'cp /etc/apt/sources.list tmp.list',
'cat tmp.list',
'sed -i "1 i\deb [trusted=yes] http://stx-stx-repomgr:80/deb-local-binary @DEBIAN_DISTRIBUTION@ main" tmp.list',
'sed -i "1 i\deb [trusted=yes] http://stx-stx-repomgr:80/deb-local-build @DEBIAN_DISTRIBUTION@ main" tmp.list',
'echo "deb @CENGNURL@/debian/debian/deb.debian.org/debian/@DEBIAN_DISTRIBUTION@-@DEBIAN_VERSION@ @DEBIAN_DISTRIBUTION@ main" >> tmp.list',
'echo "deb-src @CENGNURL@/debian/debian/deb.debian.org/debian/@DEBIAN_DISTRIBUTION@-@DEBIAN_VERSION@ @DEBIAN_DISTRIBUTION@ main" >> tmp.list',
'awk \'!a[$0]++\' tmp.list > new.list && mv -f new.list /etc/apt/sources.list',

View File

@ -10,19 +10,30 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Copyright (C) 2021 Wind River Systems,Inc
# Copyright (C) 2021-2022 Wind River Systems,Inc
#
import os
import schrootspool
import shutil
import signal
import subprocess
BUILD_ROOT = '/localdisk/loadbuild/'
STORE_ROOT = '/localdisk/pkgbuilder'
BUILD_ENGINE = 'sbuild'
DEBDIST = 'bullseye'
STX_LOCALRC = '/usr/local/bin/stx/stx-localrc'
SBUILD_CONF = '/etc/sbuild/sbuild.conf'
ENVIRON_VARS = ['OSTREE_OSNAME', 'CENGNURL', 'DEBIAN_DISTRIBUTION', 'DEBIAN_VERSION']
REPO_BUILD = 'deb-local-build'
def check_request(request_form, needed_form):
response = {}
if not all(t in request_form for t in needed_form):
response['status'] = 'fail'
msg = ','.join(needed_form)
response['msg'] = 'All required parameters are: ' + msg
return response
class Debbuilder:
@ -44,28 +55,27 @@ class Debbuilder:
Debuilder is created by python3 application 'app.py' which runs in
python Flask server to provide Restful APIs to offload the build tasks.
"""
def __init__(self, mode, logger):
self._state = 'idle'
self._mode = mode
def __init__(self, mode, dist, arch, logger):
self.logger = logger
self.chroots_pool = schrootspool.SchrootsPool(logger)
self.chroots_state = {}
self.chroot_processes = {}
self.sbuild_processes = {}
self.ctlog = None
self.attrs = {}
self.attrs['state'] = 'idle'
self.attrs['mode'] = mode
self.attrs['dist'] = dist
self.attrs['arch'] = arch
self.set_extra_repos()
self.set_environ_vars()
os.system('/opt/setup.sh')
@property
def state(self):
return self._state
@property
def mode(self):
return self._mode
@mode.setter
def mode(self, mode):
self._mode = mode
def get_state(self):
response = {}
response['status'] = 'success'
response['msg'] = self.attrs['state']
return response
def set_environ_vars(self):
if not os.path.exists(STX_LOCALRC):
@ -127,37 +137,40 @@ class Debbuilder:
def has_chroot(self, chroot):
chroots = os.popen('schroot -l')
target_line = "chroot:" + chroot
for line in chroots:
if chroot in line.strip():
if line.strip() == target_line:
self.logger.info("chroot %s exists" % chroot)
return True
return False
def add_chroot(self, user='builder', project='stx', mirror=None):
response = {}
if user == 'builder':
self._mode = 'private'
else:
self._mode = 'public'
self.logger.debug("Current chroot mode=%s" % self._mode)
def add_chroot(self, request_form):
response = check_request(request_form, ['user', 'project'])
if response:
return response
user = request_form['user']
project = request_form['project']
chroot = ''.join([DEBDIST, '-amd64-', user])
chroot = '-'.join([self.attrs['dist'], self.attrs['arch'], user])
if self.has_chroot(chroot):
self.logger.warn("chroot %s has already exists" % chroot)
self.logger.warn("chroot %s already exists" % chroot)
response['status'] = 'exists'
response['msg'] = 'chroot exists'
return response
user_dir = os.path.join(STORE_ROOT, user, project)
user_chroots_dir = os.path.join(user_dir, 'chroots')
if not os.path.exists(user_chroots_dir):
os.makedirs(user_chroots_dir)
self.logger.debug("User's chroot dir=%s" % user_chroots_dir)
os.makedirs(user_chroots_dir, exist_ok=True)
self.logger.debug("Directory of chroots: %s" % user_chroots_dir)
user_chroot = os.path.join(user_chroots_dir, chroot)
if os.path.exists(user_chroot):
self.logger.debug("Invalid chroot %s, clean it" % user_chroot)
shutil.rmtree(user_chroot)
self.logger.debug("Found disused chroot %s, remove it" % user_chroot)
try:
shutil.rmtree(user_chroot, ignore_errors=True)
except Exception as e:
self.logger.error(str(e))
# New chroot will be created below, we just reports this
self.logger.warning("Failed to remove %s" % user_chroot)
try:
self.ctlog = open(os.path.join(user_dir, 'chroot.log'), 'w')
@ -169,9 +182,9 @@ class Debbuilder:
chroot_suffix = '--chroot-suffix=-' + user
chroot_cmd = ' '.join(['sbuild-createchroot', chroot_suffix,
'--include=eatmydata', '--command-prefix=eatmydata',
DEBDIST, user_chroot])
if mirror:
chroot_cmd = ' '.join([chroot_cmd, mirror])
self.attrs['dist'], user_chroot])
if 'mirror' in request_form:
chroot_cmd = ' '.join([chroot_cmd, request_form['mirror']])
self.logger.debug("Command to creat chroot:%s" % chroot_cmd)
p = subprocess.Popen(chroot_cmd, shell=True, stdout=self.ctlog,
@ -179,105 +192,396 @@ class Debbuilder:
self.chroot_processes.setdefault(user, []).append(p)
response['status'] = 'creating'
response['msg'] = ' '.join(['please check',
user_dir + '/chroot.log'])
response['msg'] = 'Chroot creating, please check %s/chroot.log' % user_dir
return response
def load_chroot(self, user, project):
response = {}
def save_chroots_config(self, user, project):
self.logger.debug("Save the config file of chroot to persistent store")
user_conf_store_dir = os.path.join(STORE_ROOT, user, project, 'chroots/chroot.d')
system_conf_dir = '/etc/schroot/chroot.d'
try:
shutil.rmtree(user_conf_store_dir, ignore_errors=True)
shutil.copytree(system_conf_dir, user_conf_store_dir)
except Exception as e:
self.logger.error(str(e))
self.logger.error("Failed to save the config file of chroot")
else:
self.logger.info("Successfully saved the config file of chroot")
def is_parent_config(self, parent_chroot_name, target_config):
# The name of config file for the parent schroot has two parts:
# chroot_name + '-' + random number
# e.g. bullseye-amd64-user-yWJpyF
# The name of config file for the cloned schroot has three parts:
# chroot_name + '-' + random number + '-' + sequence
# e.g. bullseye-amd64-user-yWJpyF-1
conf_file_suffix = target_config.replace(parent_chroot_name + '-', '')
if '-' not in conf_file_suffix:
return True
else:
return False
def clone_chroot(self, request_form):
"""
Clone and configure multiple instances of chroots
the cloned chroot takes the sequence as suffix
The chroot index file in /etc/schroot/chroot.d also
need to be cloned to make the chroot can be managed by schroot
"""
response = check_request(request_form, ['user', 'project', 'instances'])
if response:
return response
user = request_form['user']
project = request_form['project']
required_instances = int(request_form['instances'])
chroot_sequence = 1
if required_instances < 2:
# multiple chroots are not required, just quit
self.logger.info("Only one chroot is required, skip cloning chroot")
response['status'] = 'success'
response['msg'] = 'Required instance number is %d' % required_instances
return response
# Try to find the parent chroot
user_dir = os.path.join(STORE_ROOT, user, project)
# e.g bullseye-amd64-user
parent_chroot_name = '-'.join([self.attrs['dist'], self.attrs['arch'], user])
# e.g /localdisk/pkgbuilder/user/stx/chroots/bullseye-amd64-user
parent_chroot_path = os.path.join(user_dir, 'chroots', parent_chroot_name)
if not os.path.exists(parent_chroot_path):
self.logger.error("Failed to find the parent chroot %s", parent_chroot_path)
response['status'] = 'fail'
response['msg'] = 'The parent chroot %s does not exist' % parent_chroot_path
return response
self.logger.debug("The parent chroot %s exists, start to clone chroot with it", parent_chroot_path)
for instance in range(required_instances):
cloned_chroot_name = parent_chroot_name + '-' + str(chroot_sequence)
cloned_chroot_path = parent_chroot_path + '-' + str(chroot_sequence)
if not os.path.exists(cloned_chroot_path):
try:
self.logger.info("Cloning chroot %s from the parent %s", cloned_chroot_path, parent_chroot_path)
shell_cmd = 'rm -rf %s.tmp' % cloned_chroot_path
subprocess.check_call(shell_cmd, shell=True)
shell_cmd = 'cp -ar %s %s.tmp' % (parent_chroot_path, cloned_chroot_path)
subprocess.check_call(shell_cmd, shell=True)
shell_cmd = 'mv %s.tmp %s' % (cloned_chroot_path, cloned_chroot_path)
subprocess.check_call(shell_cmd, shell=True)
except Exception as e:
self.logger.error(str(e))
response['status'] = 'fail'
if not response['msg']:
response['msg'] = 'The failed chroot instances:'
response['msg'].append(str(instance) + ' ')
continue
else:
self.logger.info("Successfully cloned chroot %s", cloned_chroot_path)
self.logger.info("Target cloned chroot %s is ready, updated config", cloned_chroot_path)
# For the cloned chroot, the schroot config file also need to be created
# Try to find the config file of parent schroot and take it as template
# e.g. it is /etc/chroots/chroot.d/bullseye-amd64-user-yWJpyF
schroot_conf_dir = os.listdir(os.path.join('/etc/schroot/chroot.d'))
for conf in schroot_conf_dir:
if self.is_parent_config(parent_chroot_name, conf):
parent_conf_name = conf
parent_conf_path = os.path.join('/etc/schroot/chroot.d', parent_conf_name)
self.logger.info("Found the config of the parent chroot: %s", parent_conf_name)
new_conf_name = parent_conf_name + '-' + str(chroot_sequence)
new_conf_path = os.path.join('/etc/schroot/chroot.d', new_conf_name)
if os.path.exists(new_conf_path):
self.logger.debug("Cloned chroot config %s already exists", new_conf_path)
chroot_sequence = chroot_sequence + 1
continue
try:
self.logger.debug("Creating config file %s from %s", new_conf_name, parent_conf_name)
shutil.copyfile(parent_conf_path, new_conf_path)
self.logger.debug("Successfully cloned chroot config, try to update %s", new_conf_name)
shell_cmd = 'sed -i \'s/%s/%s/g\' %s' % (parent_chroot_name, cloned_chroot_name, new_conf_path)
subprocess.check_call(shell_cmd, shell=True)
except Exception as e:
self.logger.error(str(e))
self.logger.error("Failed to clone and update config file %s", new_conf_path)
break
else:
self.logger.debug("Successfully cloned and updated chroot's config %s", new_conf_path)
chroot_sequence = chroot_sequence + 1
break
# Save the above chroot config files to the external persistent storage
self.save_chroots_config(user, project)
if chroot_sequence == required_instances + 1:
self.logger.info("All required %s chroots are created", str(required_instances))
response['status'] = 'success'
response['msg'] = 'All required chroots are created'
else:
self.logger.info("Not all required %d chroots created, only %d created ok",
required_instances, chroot_sequence - 1)
response['status'] = 'fail'
response['msg'] = 'Available chroots=%d' % (chroot_sequence - 1)
# Reload all chroots into the chroots pool
self.chroots_pool.load()
return response
def load_chroot(self, request_form):
response = check_request(request_form, ['user', 'project'])
if response:
return response
user = request_form['user']
project = request_form['project']
user_dir = os.path.join(STORE_ROOT, user, project)
user_chroots = os.path.join(user_dir, 'chroots/chroot.d')
if not os.path.exists(user_chroots):
self.logger.warn("Not find chroots %s" % user_chroots)
self.logger.warn("Failed to find directory of chroots %s" % user_chroots)
response['status'] = 'success'
response['msg'] = ' '.join(['External chroot', user_chroots,
'does not exist'])
else:
target_dir = '/etc/schroot/chroot.d'
if os.path.exists(target_dir):
shutil.rmtree(target_dir)
shutil.copytree(user_chroots, target_dir)
response['status'] = 'success'
response['msg'] = 'Load external chroot config ok'
try:
shutil.rmtree(target_dir, ignore_errors=True)
shutil.copytree(user_chroots, target_dir)
except Exception as e:
self.logger.error(str(e))
self.logger.error("Failed to load external config file of chroot")
response['status'] = 'fail'
response['msg'] = 'Failed to load external config file of chroot'
else:
response['status'] = 'success'
response['msg'] = 'Load external chroot config ok'
self.logger.debug("Load chroots %s" % response['status'])
self.chroots_pool.load()
return response
def save_chroot(self, user, project):
response = {}
def save_chroot(self, request_form):
response = check_request(request_form, ['user', 'project'])
if response:
return response
user = request_form['user']
project = request_form['project']
user_dir = os.path.join(STORE_ROOT, user, project)
user_chroots = os.path.join(user_dir, 'chroots/chroot.d')
if os.path.exists(user_chroots):
shutil.rmtree(user_chroots)
try:
shutil.rmtree(user_chroots, ignore_errors=True)
except Exception as e:
self.logger.error(str(e))
# Just report this but not quit
self.logger.error("Failed to remove %s", user_chroots)
sys_schroots = '/etc/schroot/chroot.d'
shutil.copytree(sys_schroots, user_chroots)
response['status'] = 'success'
response['msg'] = 'Save chroots config to external'
self.logger.debug("Save chroots config %s" % response['status'])
try:
shutil.copytree(sys_schroots, user_chroots)
except Exception as e:
self.logger.error(str(e))
self.logger.error("Failed to save %s with %s", sys_schroots, user_chroots)
response['status'] = 'fail'
response['msg'] = 'Failed to save the config files of chroots to persistent storage'
else:
response['status'] = 'success'
response['msg'] = 'Successfully saved the config files of chroots to persistent storage'
self.logger.debug("Successfully saved the config files of chroots")
return response
def add_task(self, user, proj, task_info):
response = {}
def refresh_chroots(self, request_form):
'''
Refresh all chroots with the backup 'clean' chroot
if all of them are free
'''
response = check_request(request_form, ['user', 'project'])
if response:
return response
user = request_form['user']
project = request_form['project']
chroot = ''.join([DEBDIST, '-amd64-', user])
dst_chroots = self.chroots_pool.get_idle()
if not dst_chroots:
self.logger.error('Failed to refresh chroots for some chroots are busy')
response['status'] = 'fail'
response['msg'] = 'Some chroots are busy'
return response
backup_chroot = None
user_dir = os.path.join(STORE_ROOT, user, project)
user_chroots_dir = os.path.join(user_dir, 'chroots')
for chroot in dst_chroots:
# e.g. the chroot name is 'chroot:bullseye-amd64-<user>-1'
self.logger.debug('The current chroot is %s', chroot)
chroot = chroot.split(':')[1]
self.logger.debug('The name of chroot: %s', chroot)
if not backup_chroot:
backup_chroot = chroot[0:chroot.rindex('-')]
self.logger.debug('The name of backup chroot: %s', backup_chroot)
if not os.path.exists(os.path.join(user_chroots_dir, backup_chroot)):
self.logger.error("The backup chroot %s does not exist", backup_chroot)
response['status'] = 'fail'
response['msg'] = 'The backup chroot does not exist'
return response
if backup_chroot == chroot:
continue
backup_chroot_path = os.path.join(user_chroots_dir, backup_chroot)
chroot_path = os.path.join(user_chroots_dir, chroot)
try:
cp_cmd = 'cp -ra %s %s' % (backup_chroot_path, chroot_path + '.tmp')
subprocess.check_call(cp_cmd, shell=True)
rm_cmd = 'rm -rf ' + chroot_path
subprocess.check_call(rm_cmd, shell=True)
mv_cmd = 'mv -f %s %s' % (chroot_path + '.tmp', chroot_path)
subprocess.check_call(mv_cmd, shell=True)
except subprocess.CalledProcessError as e:
self.logger.error(str(e))
self.logger.error('Failed to refresh the chroot %s', chroot)
response['status'] = 'fail'
response['msg'] = 'Error during refreshing the chroots'
return response
else:
self.logger.info('Successfully refreshed the chroot %s', chroot)
self.logger.info('Successfully refreshed all idle chroots')
response['status'] = 'success'
response['msg'] = 'All idle chroots are refreshed'
return response
def assemble_extra_repo(self, snapshot_idx):
repomgr_url = None
if not os.path.exists(STX_LOCALRC):
self.logger.warning('stx-localrc does not exist')
return None
env_list = []
with open(STX_LOCALRC) as f:
env_list = list(f)
for item in env_list:
if item.startswith('export '):
envvar = item.replace('export ', '').split('=')
if envvar and len(envvar) >= 2 and envvar[0].strip() == 'REPOMGR_DEPLOY_URL':
repomgr_url = envvar[1].strip()
break
if repomgr_url:
repomgr_url = ' '.join(['deb [trusted=yes]', repomgr_url + REPO_BUILD + '-' + snapshot_idx, self.attrs['dist'], 'main'])
self.logger.warning("The extra repository URL is %s", repomgr_url)
return repomgr_url
def add_task(self, request_form):
response = check_request(request_form,
['user', 'project', 'type', 'dsc', 'snapshot_idx'])
if response:
return response
user = request_form['user']
snapshot_index = request_form['snapshot_idx']
chroot = '-'.join([self.attrs['dist'], self.attrs['arch'], user])
if not self.has_chroot(chroot):
self.logger.critical("The chroot %s does not exist" % chroot)
self.logger.critical("The basic chroot %s does not exist" % chroot)
response['status'] = 'fail'
response['msg'] = ' '.join(['chroot', chroot, 'does not exist'])
return response
project = os.path.join(BUILD_ROOT, user, proj)
build_dir = os.path.join(project, task_info['type'],
task_info['package'])
if not os.path.isdir(build_dir):
self.logger.critical("%s does not exist" % build_dir)
# for example: dsc = '/path/to/tsconfig_1.0-1.stx.3.dsc'
dsc = request_form['dsc']
if not os.path.isfile(dsc):
self.logger.error("%s does not exist" % dsc)
response['status'] = 'fail'
response['msg'] = build_dir + ' does not exist'
response['msg'] = dsc + ' does not exist'
return response
# make sure the dsc file exists
dsc_target = os.path.join(build_dir, task_info['dsc'])
if not os.path.isfile(dsc_target):
self.logger.error("%s does not exist" % dsc_target)
bcommand = ' '.join([BUILD_ENGINE, '-d', self.attrs['dist']])
dsc_build_dir = os.path.dirname(dsc)
chroot = self.chroots_pool.apply()
self.chroots_pool.show()
if not chroot:
self.logger.error("There is not idle chroot for %s", dsc)
response['status'] = 'fail'
response['msg'] = dsc_target + ' does not exist'
response['msg'] = 'There is not idle chroot for ' + dsc
return response
self.chroots_state[dsc] = chroot
self.logger.info("Chroot %s is ready for %s", chroot, dsc)
jobs = '-j4'
if 'jobs' in task_info:
jobs = '-j' + task_info['jobs']
bcommand = ' '.join([BUILD_ENGINE, jobs, '-d', DEBDIST, '-c', chroot,
'--build-dir', build_dir, dsc_target])
self.logger.debug("Build command: %s" % bcommand)
if 'jobs' in request_form:
jobs = '-j' + request_form['jobs']
else:
jobs = '-j4'
self._state = 'works'
repo_url = self.assemble_extra_repo(snapshot_index)
extra_repo = '--extra-repository=\'%s\'' % (repo_url)
bcommand = ' '.join([bcommand, jobs, '-c', chroot, extra_repo,
'--build-dir', dsc_build_dir, dsc])
self.logger.debug("Build command: %s" % (bcommand))
self.attrs['state'] = 'works'
# verify if tests need to be executed
if task_info['run_tests'] == 'True':
p = subprocess.Popen(bcommand, shell=True)
if request_form['run_tests'] == 'True':
p = subprocess.Popen(bcommand, shell=True, preexec_fn=os.setsid)
else:
self.logger.debug("No tests needed, setting DEB_BUILD_OPTIONS=nocheck")
p = subprocess.Popen(bcommand, shell=True, env={**os.environ, 'DEB_BUILD_OPTIONS': 'nocheck'})
self.sbuild_processes.setdefault(user, []).append(p)
p = subprocess.Popen(bcommand, shell=True, env={**os.environ, 'DEB_BUILD_OPTIONS': 'nocheck'}, preexec_fn=os.setsid)
self.sbuild_processes.setdefault(user, {}).setdefault(dsc, p)
response['status'] = 'success'
response['msg'] = 'sbuild package building task launched'
response['msg'] = chroot
return response
def kill_task(self, user, owner):
response = {}
def clean_stamp(self, request_form):
response = check_request(request_form, ['user', 'project', 'type'])
if response:
return response
if owner in ['sbuild', 'all']:
if self.sbuild_processes and self.sbuild_processes[user]:
for p in self.sbuild_processes[user]:
self.logger.debug("Terminating package build process")
p.terminate()
p.wait()
self.logger.debug("Package build process terminated")
del self.sbuild_processes[user]
user = request_form['user']
project = request_form['project']
build_type = request_form['type']
stamp_dir = os.path.join(STORE_ROOT, user, project, build_type, 'stamp')
try:
shutil.rmtree(stamp_dir, ignore_errors=True)
except Exception as e:
self.logger.error(str(e))
# New chroot will be created below, we just reports this
self.logger.warning("Failed to remove %s" % stamp_dir)
response['status'] = 'fail'
response['msg'] = 'Failed to remove stamp directory'
else:
self.logger.info("The stamp directory %s has been cleaned", stamp_dir)
response['status'] = 'success'
response['msg'] = 'Successfully cleaned the stamp directory'
return response
def kill_task(self, request_form):
response = check_request(request_form, ['user', 'owner'])
if response:
return response
user = request_form['user']
owner = request_form['owner']
if 'dsc' in request_form:
done_dsc = request_form['dsc']
if done_dsc:
self.chroots_pool.release(self.chroots_state[done_dsc])
self.logger.debug('The chroot %s for %s is released', self.chroots_state[done_dsc], done_dsc)
for dsckey in self.sbuild_processes[user].keys():
if dsckey == done_dsc:
self.logger.debug("Terminating package build process for %s", dsckey)
p = self.sbuild_processes[user][dsckey]
os.killpg(os.getpgid(p.pid), signal.SIGTERM)
self.logger.debug("Package build process terminated for %s", dsckey)
del self.sbuild_processes[user][dsckey]
break
else:
if owner in ['sbuild', 'all']:
self.chroots_pool.show()
if self.sbuild_processes and self.sbuild_processes[user]:
for dsckey in self.sbuild_processes[user].keys():
self.logger.debug("Terminating package build process for %s", dsckey)
p = self.sbuild_processes[user][dsckey]
os.killpg(os.getpgid(p.pid), signal.SIGTERM)
self.logger.debug("chroot:%s ---> %s", self.chroots_state[dsckey], dsckey)
self.chroots_pool.release(self.chroots_state[dsckey])
self.logger.debug("Package build process terminated")
del self.sbuild_processes[user]
if owner in ['chroot', 'all']:
if self.ctlog:
@ -290,25 +594,31 @@ class Debbuilder:
self.logger.debug("Chroot process terminated")
del self.chroot_processes[user]
self.logger.info("Current status of chroots:")
self.chroots_pool.show()
response['status'] = 'success'
response['msg'] = 'killed all build related tasks'
return response
def stop_task(self, user):
response = {}
# check whether the need schroot exists
chroot = ''.join([DEBDIST, '-amd64-', user])
if 'public' in self.mode:
self.logger.debug("Public mode, chroot:%s" % chroot)
else:
self.logger.debug("Private mode, chroot:%s" % chroot)
def stop_task(self, request_form):
req = {}
response = check_request(request_form, ['user'])
if response:
return response
user = request_form['user']
# check whether the need schroot exists
chroot = '-'.join([self.attrs['dist'], self.attrs['arch'], user])
if not self.has_chroot(chroot):
self.logger.critical("No required chroot %s" % chroot)
self.kill_task(user, 'all')
req['user'] = user
req['owner'] = 'all'
self.kill_task(req)
os.system('sbuild_abort')
response['status'] = 'success'
response['msg'] = 'Stop current build tasks'
self.attrs['state'] = 'idle'
return response

View File

@ -0,0 +1,121 @@
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Copyright (C) 2022 Wind River Systems,Inc
#
import logging
import subprocess
SCHROOTS_CONFIG = '/etc/schroot/chroot.d/'
class Schroot:
def __init__(self, name, state='idle'):
self.name = name
self.state = state
def is_idle(self):
if self.state == 'idle':
return True
return False
def set_busy(self):
self.state = 'work'
def get_name(self):
return self.name
class SchrootsPool:
"""
schrootsPool manages all the schroots in current container
The schroots listed by schroot -l will be registered
and assigned the build task
"""
def __init__(self, logger):
self.schroots = []
self.logger = logger
def exists(self, name):
for schroot in self.schroots:
if schroot.name == name:
return True
return False
def load(self):
schroots = subprocess.run(['schroot', '-l'], stdout=subprocess.PIPE,
universal_newlines=True).stdout.splitlines()
if len(schroots) < 1:
self.logger.error('There are no schroots found, exit')
return False
for sname in schroots:
# Filter 'chroot:bullseye-amd64-<user>' as the backup chroot
if len(sname.split('-')) >= 4 and not self.exists(sname):
self.schroots.append(Schroot(sname.strip(), 'idle'))
return True
def apply(self):
self.logger.debug("schroot pool status:")
self.show()
for schroot in self.schroots:
if schroot.is_idle():
schroot.set_busy()
self.logger.debug('%s has been assigned', schroot.name)
return schroot.name
self.logger.debug("No idle schroot can be used")
return None
def release(self, name):
for schroot in self.schroots:
if schroot.name == name.strip():
# Fixme, whether need to end session here
schroot.state = 'idle'
self.logger.debug('%s has been released', name)
def get_idle(self):
idle_schroots = []
for schroot in self.schroots:
schroot_name = schroot.get_name()
if not schroot.is_idle():
self.logger.error('schroot %s is busy and can not be refreshed', schroot_name)
continue
idle_schroots.append(schroot_name)
self.logger.debug('schroot %s is idle and can be refreshed', schroot_name)
return idle_schroots
def release_all(self):
for schroot in self.schroots:
# Fixme, whether need to end session here
schroot.state = 'idle'
self.logger.debug('All chroots has been released')
def show(self):
for schroot in self.schroots:
self.logger.info("schroot name:%s state:%s", schroot.name, schroot.state)
if __name__ == "__main__":
"""
For unit tests
"""
logger = logging.getLogger('schrootPool')
logger.setLevel(logging.DEBUG)
schroots_pool = SchrootsPool(logger)
schroots_pool.load()
s0 = schroots_pool.apply()
s1 = schroots_pool.apply()
s2 = schroots_pool.apply()
schroots_pool.show()
schroots_pool.release(s0)
schroots_pool.release(s1)
schroots_pool.show()