/usr/lib/koji-hub-plugins/runroot.py is in koji-servers 1.10.0-1.
This file is owned by root:root, with mode 0o644.
The actual contents of the file can be viewed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 | # kojid plugin
import commands
import koji
import ConfigParser
import os
import platform
compat_mode = False
try:
import koji.tasks as tasks
from koji.tasks import scan_mounts
from koji.util import isSuccess as _isSuccess
from koji.util import parseStatus as _parseStatus
from koji.daemon import log_output
from __main__ import BuildRoot
except ImportError:
compat_mode = True
#old way
import tasks
#XXX - stuff we need from kojid
from __main__ import BuildRoot, log_output, scan_mounts, _isSuccess, _parseStatus
__all__ = ('RunRootTask',)
CONFIG_FILE = '/etc/kojid/runroot.conf'
class RunRootTask(tasks.BaseTaskHandler):
Methods = ['runroot']
_taskWeight = 2.0
def __init__(self, *args, **kwargs):
self._read_config()
return super(RunRootTask, self).__init__(*args, **kwargs)
def _get_path_params(self, path, rw=False):
found = False
for mount_data in self.config['paths']:
if path.startswith(mount_data['mountpoint']):
found = True
break
if not found:
raise koji.GenericError("bad config: missing corresponding mountpoint")
options = []
for o in mount_data['options'].split(','):
if rw and o == 'ro':
options.append('rw')
else:
options.append(o)
rel_path = path[len(mount_data['mountpoint']):]
rel_path = rel_path[1:] if rel_path.startswith('/') else rel_path
res = (os.path.join(mount_data['path'], rel_path), path, mount_data['fstype'], ','.join(options))
return res
def _read_config(self):
cp = ConfigParser.SafeConfigParser()
cp.read(CONFIG_FILE)
self.config = {
'default_mounts': [],
'safe_roots': [],
'path_subs': [],
'paths': [],
}
if cp.has_option('paths', 'default_mounts'):
self.config['default_mounts'] = cp.get('paths', 'default_mounts').split(',')
if cp.has_option('paths', 'safe_roots'):
self.config['safe_roots'] = cp.get('paths', 'safe_roots').split(',')
if cp.has_option('paths', 'path_subs'):
self.config['path_subs'] = [x.split(',') for x in cp.get('paths', 'path_subs').split('\n')]
count = 0
while True:
section_name = 'path%d' % count
if not cp.has_section(section_name):
break
try:
self.config['paths'].append({
'mountpoint': cp.get(section_name, 'mountpoint'),
'path': cp.get(section_name, 'path'),
'fstype': cp.get(section_name, 'fstype'),
'options': cp.get(section_name, 'options'),
})
except ConfigParser.NoOptionError:
raise koji.GenericError("bad config: missing options in %s section" % section_name)
count += 1
for path in self.config['default_mounts'] + self.config['safe_roots'] + [x[0] for x in self.config['path_subs']]:
if not path.startswith('/'):
raise koji.GenericError("bad config: all paths (default_mounts, safe_roots, path_subs) needs to be absolute: %s" % path)
def handler(self, root, arch, command, keep=False, packages=[], mounts=[], repo_id=None, skip_setarch=False, weight=None, upload_logs=None):
"""Create a buildroot and run a command (as root) inside of it
Command may be a string or a list.
Returns a message indicating success if the command was successful, and
raises an error otherwise. Command output will be available in
runroot.log in the task output directory on the hub.
skip_setarch is a rough approximation of an old hack
the keep option is not used. keeping for compatibility for now...
upload_logs is list of absolute paths which will be uploaded for
archiving on hub. It always consists of /tmp/runroot.log, but can be
used for additional logs (pungi.log, etc.)
"""
if weight is not None:
weight = max(weight, 0.5)
self.session.host.setTaskWeight(self.id, weight)
#noarch is funny
if arch == "noarch":
#we need a buildroot arch. Pick one that:
# a) this host can handle
# b) the build tag can support
# c) is canonical
host_arches = self.session.host.getHost()['arches']
if not host_arches:
raise koji.BuildError, "No arch list for this host"
tag_arches = self.session.getBuildConfig(root)['arches']
if not tag_arches:
raise koji.BuildError, "No arch list for tag: %s" % root
#index canonical host arches
host_arches = dict([(koji.canonArch(a),1) for a in host_arches.split()])
#pick the first suitable match from tag's archlist
for br_arch in tag_arches.split():
br_arch = koji.canonArch(br_arch)
if host_arches.has_key(br_arch):
#we're done
break
else:
#no overlap
raise koji.BuildError, "host does not match tag arches: %s (%s)" % (root, tag_arches)
else:
br_arch = arch
if repo_id:
repo_info = self.session.repoInfo(repo_id, strict=True)
if repo_info['tag_name'] != root:
raise koji.BuildError, "build tag (%s) does not match repo tag (%s)" % (root, repo_info['tag_name'])
if repo_info['state'] not in (koji.REPO_STATES['READY'], koji.REPO_STATES['EXPIRED']):
raise koji.BuildError, "repos in the %s state may not be used by runroot" % koji.REPO_STATES[repo_info['state']]
else:
repo_info = self.session.getRepo(root)
if not repo_info:
#wait for it
task_id = self.session.host.subtask(method='waitrepo',
arglist=[root, None, None],
parent=self.id)
repo_info = self.wait(task_id)[task_id]
if compat_mode:
broot = BuildRoot(root, br_arch, self.id, repo_id=repo_info['id'])
else:
broot = BuildRoot(self.session, self.options, root, br_arch, self.id, repo_id=repo_info['id'])
broot.workdir = self.workdir
broot.init()
rootdir = broot.rootdir()
#workaround for rpm oddness
os.system('rm -f "%s"/var/lib/rpm/__db.*' % rootdir)
#update buildroot state (so that updateBuildRootList() will work)
self.session.host.setBuildRootState(broot.id, 'BUILDING')
try:
if packages:
#pkglog = '%s/%s' % (broot.resultdir(), 'packages.log')
pkgcmd = ['--install'] + packages
status = broot.mock(pkgcmd)
self.session.host.updateBuildRootList(broot.id, broot.getPackageList())
if not _isSuccess(status):
raise koji.BuildrootError, _parseStatus(status, pkgcmd)
if isinstance(command, str):
cmdstr = command
else:
#we were passed an arglist
#we still have to run this through the shell (for redirection)
#but we can preserve the list structure precisely with careful escaping
cmdstr = ' '.join(["'%s'" % arg.replace("'", r"'\''") for arg in command])
# A nasty hack to put command output into its own file until mock can be
# patched to do something more reasonable than stuff everything into build.log
cmdargs = ['/bin/sh', '-c', "{ %s; } < /dev/null 2>&1 | /usr/bin/tee /tmp/runroot.log; exit ${PIPESTATUS[0]}" % cmdstr]
# always mount /mnt/redhat (read-only)
# always mount /mnt/iso (read-only)
# also need /dev bind mount
self.do_mounts(rootdir, [self._get_path_params(x) for x in self.config['default_mounts']])
self.do_extra_mounts(rootdir, mounts)
mock_cmd = ['chroot']
if skip_setarch:
#we can't really skip it, but we can set it to the current one instead of of the chroot one
myarch = platform.uname()[5]
mock_cmd.extend(['--arch', myarch])
mock_cmd.append('--')
mock_cmd.extend(cmdargs)
rv = broot.mock(mock_cmd)
log_paths = ['/tmp/runroot.log']
if upload_logs is not None:
log_paths += upload_logs
for log_path in log_paths:
self.uploadFile(rootdir + log_path)
finally:
# mock should umount its mounts, but it will not handle ours
self.undo_mounts(rootdir, fatal=False)
broot.expire()
if isinstance(command, str):
cmdlist = command.split()
else:
cmdlist = command
cmdlist = [param for param in cmdlist if '=' not in param]
if cmdlist:
cmd = os.path.basename(cmdlist[0])
else:
cmd = '(none)'
if _isSuccess(rv):
return '%s completed successfully' % cmd
else:
raise koji.BuildrootError, _parseStatus(rv, cmd)
def do_extra_mounts(self, rootdir, mounts):
mnts = []
for mount in mounts:
mount = os.path.normpath(mount)
for safe_root in self.config['safe_roots']:
if mount.startswith(safe_root):
break
else:
#no match
raise koji.GenericError("read-write mount point is not safe: %s" % mount)
#normpath should have removed any .. dirs, but just in case...
if mount.find('/../') != -1:
raise koji.GenericError("read-write mount point is not safe: %s" % mount)
for re, sub in self.config['path_subs']:
mount = mount.replace(re, sub)
mnts.append(self._get_path_params(mount, rw=True))
self.do_mounts(rootdir, mnts)
def do_mounts(self, rootdir, mounts):
if not mounts:
return
self.logger.info('New runroot')
self.logger.info("Runroot mounts: %s" % mounts)
fn = '%s/tmp/runroot_mounts' % rootdir
fslog = file(fn, 'a')
logfile = "%s/do_mounts.log" % self.workdir
uploadpath = self.getUploadDir()
error = None
for dev,path,type,opts in mounts:
if not path.startswith('/'):
raise koji.GenericError("invalid mount point: %s" % path)
mpoint = "%s%s" % (rootdir,path)
if opts is None:
opts = []
else:
opts = opts.split(',')
if 'bind' in opts:
#make sure dir exists
if not os.path.isdir(dev):
error = koji.GenericError("No such directory or mount: %s" % dev)
break
type = 'none'
if path is None:
#shorthand for "same path"
path = dev
if 'bg' in opts:
error = koji.GenericError("bad config: background mount not allowed")
break
opts = ','.join(opts)
cmd = ['mount', '-t', type, '-o', opts, dev, mpoint]
self.logger.info("Mount command: %r" % cmd)
koji.ensuredir(mpoint)
if compat_mode:
status = log_output(cmd[0], cmd, logfile, uploadpath, logerror=True, append=True)
else:
status = log_output(self.session, cmd[0], cmd, logfile, uploadpath, logerror=True, append=True)
if not _isSuccess(status):
error = koji.GenericError("Unable to mount %s: %s" \
% (mpoint, _parseStatus(status, cmd)))
break
fslog.write("%s\n" % mpoint)
fslog.flush()
fslog.close()
if error is not None:
self.undo_mounts(rootdir, fatal=False)
raise error
def undo_mounts(self, rootdir, fatal=True):
self.logger.debug("Unmounting runroot mounts")
mounts = {}
fn = '%s/tmp/runroot_mounts' % rootdir
if os.path.exists(fn):
fslog = file(fn,'r')
for line in fslog:
mounts.setdefault(line.strip(), 1)
fslog.close()
#also, check /proc/mounts just in case
for dir in scan_mounts(rootdir):
mounts.setdefault(dir, 1)
mounts = mounts.keys()
# deeper directories first
mounts.sort()
mounts.reverse()
failed = []
self.logger.info("Unmounting (runroot): %s" % mounts)
for dir in mounts:
(rv, output) = commands.getstatusoutput("umount -l '%s'" % dir)
if rv != 0:
failed.append("%s: %s" % (dir, output))
if failed:
msg = "Unable to unmount: %s" % ', '.join(failed)
self.logger.warn(msg)
if fatal:
raise koji.GenericError, msg
else:
# remove the mount list when everything is unmounted
try:
os.unlink(fn)
except OSError:
pass
|