Merge "Debian: pkgbuilder: Support parallel build with multiple chroots"
This commit is contained in:
commit
416962d788
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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',
|
||||
|
@ -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
|
||||
|
121
stx/toCOPY/pkgbuilder/schrootspool.py
Normal file
121
stx/toCOPY/pkgbuilder/schrootspool.py
Normal 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()
|
Loading…
x
Reference in New Issue
Block a user