This file is indexed.

/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