You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
384 lines
14 KiB
Python
384 lines
14 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
|
|
# This program is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation; version 2 of the License.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU Library General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; if not, see <https://gnu.org/licenses/>.
|
|
|
|
import os
|
|
import re
|
|
import six
|
|
from six.moves import shlex_quote
|
|
import kobo.log
|
|
from kobo.shortcuts import run
|
|
|
|
from pungi.wrappers import kojiwrapper
|
|
|
|
|
|
RUNROOT_TYPES = ["local", "koji", "openssh"]
|
|
|
|
|
|
class Runroot(kobo.log.LoggingBase):
|
|
def __init__(self, compose, logger=None, phase=None):
|
|
"""
|
|
Creates new Runroot instance.
|
|
|
|
:param Compose compose: Compose instance.
|
|
:param Logger logger: Logger instance to log message to.
|
|
:param str phase: Pungi phase the runroot task is run as part of.
|
|
"""
|
|
kobo.log.LoggingBase.__init__(self, logger=logger)
|
|
self.compose = compose
|
|
self.runroot_method = self.get_runroot_method(phase)
|
|
# Holds the result of last `run()` call.
|
|
self._result = None
|
|
|
|
def get_runroot_method(self, phase=None):
|
|
"""
|
|
Returns the runroot method by checking the `runroot_tag` and
|
|
`runroot_method` options in configuration.
|
|
|
|
:param str phase: Pungi phase to get the runroot method for.
|
|
|
|
:return: The configured method
|
|
"""
|
|
runroot_tag = self.compose.conf.get("runroot_tag")
|
|
runroot_method = self.compose.conf.get("runroot_method")
|
|
if runroot_tag and not runroot_method:
|
|
# If we have runroot tag and no method, let's assume koji method
|
|
# for backwards compatibility.
|
|
return "koji"
|
|
|
|
if isinstance(runroot_method, dict):
|
|
# If runroot_method is set to dict, check if there is runroot_method
|
|
# override for the current phase.
|
|
if phase in runroot_method:
|
|
return runroot_method[phase]
|
|
global_runroot_method = self.compose.conf.get("global_runroot_method")
|
|
return global_runroot_method or "local"
|
|
|
|
# Otherwise use the configured method or default to local if nothing is
|
|
# given.
|
|
return runroot_method or "local"
|
|
|
|
def _run_local(self, command, log_file=None, **kwargs):
|
|
"""
|
|
Runs the runroot command locally.
|
|
"""
|
|
run(command, show_cmd=True, logfile=log_file)
|
|
self._result = True
|
|
|
|
def _has_losetup_error(self, log_dir):
|
|
"""
|
|
Check if there's losetup error in log.
|
|
|
|
This error happens if the Koji builder runs out of loopback devices.
|
|
This can happen if too many tasks that require them are scheduled on
|
|
the same builder. A retried task might end up on a different builder,
|
|
or maybe some other task will have finished already.
|
|
|
|
:param str log_dir: path to buildinstall log dir,
|
|
e.g. logs/s390x/buildinstall-BaseOS-logs/
|
|
"""
|
|
if not log_dir:
|
|
return False
|
|
|
|
log_file = os.path.join(log_dir, "program.log")
|
|
try:
|
|
with open(log_file) as f:
|
|
for line in f.readlines():
|
|
if "losetup: cannot find an unused loop device" in line:
|
|
return True
|
|
if re.match("losetup: .* failed to set up loop device", line):
|
|
return True
|
|
except Exception:
|
|
pass
|
|
return False
|
|
|
|
def _run_koji(self, command, log_file=None, packages=None, arch=None, **kwargs):
|
|
"""
|
|
Runs the runroot command in Koji.
|
|
"""
|
|
runroot_channel = self.compose.conf.get("runroot_channel")
|
|
runroot_tag = self.compose.conf["runroot_tag"]
|
|
log_dir = kwargs.pop("log_dir", None)
|
|
|
|
koji_wrapper = kojiwrapper.KojiWrapper(self.compose)
|
|
koji_cmd = koji_wrapper.get_runroot_cmd(
|
|
runroot_tag,
|
|
arch,
|
|
command,
|
|
channel=runroot_channel,
|
|
use_shell=True,
|
|
packages=packages,
|
|
**kwargs
|
|
)
|
|
|
|
attempt = 0
|
|
max_retries = 3
|
|
while True:
|
|
output = koji_wrapper.run_runroot_cmd(koji_cmd, log_file=log_file)
|
|
if output["retcode"] == 0:
|
|
self._result = output
|
|
return
|
|
elif attempt >= max_retries or not self._has_losetup_error(log_dir):
|
|
raise RuntimeError(
|
|
"Runroot task failed: %s. See %s for more details."
|
|
% (output["task_id"], log_file)
|
|
)
|
|
attempt += 1
|
|
|
|
def _ssh_run(self, hostname, user, command, fmt_dict=None, log_file=None):
|
|
"""
|
|
Helper method to run the command using "ssh".
|
|
|
|
:param str hostname: Hostname.
|
|
:param str user: User for login.
|
|
:param str command: Command to run.
|
|
:param str fmt_dict: If set, the `command` is formatted like
|
|
`command.format(**fmt_dict)`.
|
|
:param str log_file: Log file.
|
|
:return str: Output of remote command.
|
|
"""
|
|
formatted_cmd = command.format(**fmt_dict) if fmt_dict else command
|
|
ssh_cmd = ["ssh", "-oBatchMode=yes", "-n", "-l", user, hostname, formatted_cmd]
|
|
output = run(ssh_cmd, show_cmd=True, logfile=log_file)[1]
|
|
if six.PY3 and isinstance(output, bytes):
|
|
return output.decode()
|
|
else:
|
|
return output
|
|
|
|
def _log_file(self, base, suffix):
|
|
return base.replace(".log", "." + suffix + ".log")
|
|
|
|
def _run_openssh(
|
|
self,
|
|
command,
|
|
log_file=None,
|
|
arch=None,
|
|
packages=None,
|
|
chown_paths=None,
|
|
**kwargs
|
|
):
|
|
"""
|
|
Runs the runroot command on remote machine using ssh.
|
|
"""
|
|
runroot_ssh_hostnames = self.compose.conf.get("runroot_ssh_hostnames", {})
|
|
if arch not in runroot_ssh_hostnames:
|
|
raise ValueError("The arch %r not in runroot_ssh_hostnames." % arch)
|
|
|
|
# If the output dir is defined, change the permissions of files generated
|
|
# by the runroot task, so the Pungi user can access them.
|
|
if chown_paths:
|
|
paths = " ".join(shlex_quote(pth) for pth in chown_paths)
|
|
command += " ; EXIT_CODE=$?"
|
|
# Make the files world readable
|
|
command += " ; chmod -R a+r %s" % paths
|
|
# and owned by the same user that is running the process
|
|
command += " ; chown -R %d %s" % (os.getuid(), paths)
|
|
# Exit with code of main command
|
|
command += " ; exit $EXIT_CODE"
|
|
|
|
hostname = runroot_ssh_hostnames[arch]
|
|
user = self.compose.conf.get("runroot_ssh_username", "root")
|
|
runroot_tag = self.compose.conf["runroot_tag"]
|
|
init_template = self.compose.conf.get("runroot_ssh_init_template")
|
|
install_packages_template = self.compose.conf.get(
|
|
"runroot_ssh_install_packages_template"
|
|
)
|
|
run_template = self.compose.conf.get("runroot_ssh_run_template")
|
|
|
|
# Init the runroot on remote machine and get the runroot_key.
|
|
if init_template:
|
|
fmt_dict = {"runroot_tag": runroot_tag}
|
|
runroot_key = self._ssh_run(
|
|
hostname,
|
|
user,
|
|
init_template,
|
|
fmt_dict,
|
|
log_file=self._log_file(log_file, "init"),
|
|
)
|
|
runroot_key = runroot_key.rstrip("\n\r")
|
|
else:
|
|
runroot_key = None
|
|
|
|
# Install the packages needed for runroot task if configured.
|
|
if install_packages_template and packages:
|
|
fmt_dict = {"packages": " ".join(packages)}
|
|
if runroot_key:
|
|
fmt_dict["runroot_key"] = runroot_key
|
|
self._ssh_run(
|
|
hostname,
|
|
user,
|
|
install_packages_template,
|
|
fmt_dict,
|
|
log_file=self._log_file(log_file, "install_packages"),
|
|
)
|
|
|
|
# Run the runroot task and get the buildroot RPMs.
|
|
if run_template:
|
|
fmt_dict = {"command": command}
|
|
if runroot_key:
|
|
fmt_dict["runroot_key"] = runroot_key
|
|
self._ssh_run(hostname, user, run_template, fmt_dict, log_file=log_file)
|
|
|
|
fmt_dict[
|
|
"command"
|
|
] = "rpm -qa --qf='%{name}-%{version}-%{release}.%{arch}\n'"
|
|
buildroot_rpms = self._ssh_run(
|
|
hostname,
|
|
user,
|
|
run_template,
|
|
fmt_dict,
|
|
log_file=self._log_file(log_file, "rpms"),
|
|
)
|
|
else:
|
|
self._ssh_run(hostname, user, command, log_file=log_file)
|
|
buildroot_rpms = self._ssh_run(
|
|
hostname,
|
|
user,
|
|
"rpm -qa --qf='%{name}-%{version}-%{release}.%{arch}\n'",
|
|
log_file=self._log_file(log_file, "rpms"),
|
|
)
|
|
|
|
# Parse the buildroot_rpms and store it in self._result.
|
|
self._result = []
|
|
for i in buildroot_rpms.splitlines():
|
|
if not i:
|
|
continue
|
|
self._result.append(i)
|
|
|
|
def run(self, command, log_file=None, packages=None, arch=None, **kwargs):
|
|
"""
|
|
Runs the runroot task using the `Runroot.runroot_method`. Blocks until
|
|
the runroot task is successfully finished. Raises an exception on error.
|
|
|
|
The **kwargs are optional and matches the `KojiWrapper.get_runroot_cmd()`
|
|
kwargs. Some `runroot_method` methods might ignore the kwargs which
|
|
do not make sense for them.
|
|
|
|
:param str command: Command to execute.
|
|
:param str log_file: Log file into which the output of runroot task will
|
|
be logged.
|
|
:param list packages: List of packages which are needed for runroot task
|
|
to be executed.
|
|
:param str arch: Architecture on which the runroot task should be
|
|
executed.
|
|
:param str output_dir: Directory where the `command` stores its output.
|
|
The permissions of this output_dir might be changed by `runroot_method`
|
|
to allow the executor of this runroot task to access them.
|
|
See `KojiWrapper.get_runroot_cmd()` for more information.
|
|
"""
|
|
if self.runroot_method == "local":
|
|
self._run_local(
|
|
command, log_file=log_file, packages=packages, arch=arch, **kwargs
|
|
)
|
|
elif self.runroot_method == "koji":
|
|
self._run_koji(
|
|
command, log_file=log_file, packages=packages, arch=arch, **kwargs
|
|
)
|
|
elif self.runroot_method == "openssh":
|
|
self._run_openssh(
|
|
command, log_file=log_file, packages=packages, arch=arch, **kwargs
|
|
)
|
|
else:
|
|
raise ValueError("Unknown runroot_method %r." % self.runroot_method)
|
|
|
|
def run_pungi_buildinstall(self, args, log_file=None, arch=None, **kwargs):
|
|
"""
|
|
Runs the Lorax buildinstall runroot command using the Pungi Buildinstall
|
|
Koji plugin as pungi_buildinstall task.
|
|
|
|
The **kwargs are optional and matches the
|
|
`KojiWrapper.get_pungi_buildinstall_cmd()` kwargs.
|
|
|
|
:param dict args: Arguments for the pungi_buildinstall Koji task.
|
|
:param str log_file: Log file into which the output of the task will
|
|
be logged.
|
|
:param str arch: Architecture on which the task should be executed.
|
|
"""
|
|
runroot_channel = self.compose.conf.get("runroot_channel")
|
|
runroot_tag = self.compose.conf["runroot_tag"]
|
|
|
|
koji_wrapper = kojiwrapper.KojiWrapper(self.compose)
|
|
koji_cmd = koji_wrapper.get_pungi_buildinstall_cmd(
|
|
runroot_tag,
|
|
arch,
|
|
args,
|
|
channel=runroot_channel,
|
|
chown_uid=os.getuid(),
|
|
**kwargs
|
|
)
|
|
|
|
output = koji_wrapper.run_runroot_cmd(koji_cmd, log_file=log_file)
|
|
if output["retcode"] != 0:
|
|
raise RuntimeError(
|
|
"Pungi-buildinstall task failed: %s. See %s for more details."
|
|
% (output["task_id"], log_file)
|
|
)
|
|
self._result = output
|
|
|
|
def run_pungi_ostree(self, args, log_file=None, arch=None, **kwargs):
|
|
"""
|
|
Runs the OStree runroot command using the Pungi OSTree
|
|
Koji plugin as pungi_ostree task.
|
|
|
|
The **kwargs are optional and matches the
|
|
`KojiWrapper.get_pungi_buildinstall_cmd()` kwargs.
|
|
|
|
:param dict args: Arguments for the pungi_ostree Koji task.
|
|
:param str log_file: Log file into which the output of the task will
|
|
be logged.
|
|
:param str arch: Architecture on which the task should be executed.
|
|
"""
|
|
runroot_channel = self.compose.conf.get("runroot_channel")
|
|
runroot_tag = self.compose.conf["runroot_tag"]
|
|
|
|
koji_wrapper = kojiwrapper.KojiWrapper(self.compose)
|
|
koji_cmd = koji_wrapper.get_pungi_ostree_cmd(
|
|
runroot_tag, arch, args, channel=runroot_channel, **kwargs
|
|
)
|
|
|
|
output = koji_wrapper.run_runroot_cmd(koji_cmd, log_file=log_file)
|
|
if output["retcode"] != 0:
|
|
raise RuntimeError(
|
|
"Pungi-buildinstall task failed: %s. See %s for more details."
|
|
% (output["task_id"], log_file)
|
|
)
|
|
self._result = output
|
|
|
|
def get_buildroot_rpms(self):
|
|
"""
|
|
Returns the list of RPMs installed in a buildroot in which the runroot
|
|
task was executed. This is needed to track what actually generated
|
|
the data generated by runroot task.
|
|
|
|
This must be called after the `run()` method successfully finished,
|
|
otherwise raises an exception.
|
|
|
|
:return: List of RPMs in buildroot in which the runroot task run.
|
|
"""
|
|
if not self._result:
|
|
raise ValueError(
|
|
"Runroot.get_buildroot_rpms called before runroot task finished."
|
|
)
|
|
if self.runroot_method in ["local", "koji"]:
|
|
if self.runroot_method == "local":
|
|
task_id = None
|
|
else:
|
|
task_id = self._result["task_id"]
|
|
return kojiwrapper.get_buildroot_rpms(self.compose, task_id)
|
|
elif self.runroot_method == "openssh":
|
|
# For openssh runroot_method, the result is list of buildroot_rpms.
|
|
return self._result
|
|
else:
|
|
raise ValueError("Unknown runroot_method %r." % self.runroot_method)
|