diff --git a/Makefile b/Makefile index b9c23bf2..d5e9b776 100644 --- a/Makefile +++ b/Makefile @@ -105,6 +105,7 @@ force-tag:: # @$(MAKE) tag TAG_OPTS="-F $(TAG_OPTS)" DESTDIR ?= / +TYPE = systemd install: @if [ "$(DESTDIR)" = "" ]; then \ echo " "; \ @@ -115,4 +116,4 @@ install: mkdir -p $(DESTDIR) for d in $(SUBDIRS); do make DESTDIR=`cd $(DESTDIR); pwd` \ - -C $$d install; [ $$? = 0 ] || exit 1; done + -C $$d install TYPE=$(TYPE); [ $$? = 0 ] || exit 1; done diff --git a/builder/Makefile b/builder/Makefile index 02bae119..1de0a9cd 100644 --- a/builder/Makefile +++ b/builder/Makefile @@ -1,6 +1,7 @@ - BINFILES = kojid LIBEXECFILES = mergerepos +SYSTEMDSYSTEMUNITDIR = $(shell pkg-config systemd --variable=systemdsystemunitdir) +TYPE = systemd _default: @echo "nothing to make. try make install" @@ -9,7 +10,7 @@ clean: rm -f *.o *.so *.pyc *~ -install: +_install: @if [ "$(DESTDIR)" = "" ]; then \ echo " "; \ echo "ERROR: A destdir is required"; \ @@ -23,13 +24,19 @@ install: install -p -m 755 $(LIBEXECFILES) $(DESTDIR)/usr/libexec/kojid mkdir -p $(DESTDIR)/etc/mock/koji + + mkdir -p $(DESTDIR)/etc/kojid + install -p -m 644 kojid.conf $(DESTDIR)/etc/kojid/kojid.conf + +install-systemd: _install + mkdir -p $(DESTDIR)$(SYSTEMDSYSTEMUNITDIR) + install -p -m 644 kojid.service $(DESTDIR)$(SYSTEMDSYSTEMUNITDIR) + +install-sysv: _install mkdir -p $(DESTDIR)/etc/rc.d/init.d install -p -m 755 kojid.init $(DESTDIR)/etc/rc.d/init.d/kojid mkdir -p $(DESTDIR)/etc/sysconfig install -p -m 644 kojid.sysconfig $(DESTDIR)/etc/sysconfig/kojid - mkdir -p $(DESTDIR)/etc/kojid - install -p -m 644 kojid.conf $(DESTDIR)/etc/kojid/kojid.conf - - +install: install-$(TYPE) diff --git a/builder/kojid b/builder/kojid index 3df05884..b6b79c16 100755 --- a/builder/kojid +++ b/builder/kojid @@ -5,7 +5,7 @@ # # Koji is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; +# License as published by the Free Software Foundation; # version 2.1 of the License. # # This software is distributed in the hope that it will be useful, @@ -250,12 +250,10 @@ class BuildRoot(object): entries = [] if plugin: tag_name = 'pluginRepository' - id_suffix = 'plugin-repo' - name_prefix = 'Plugin repository for Koji' else: tag_name = 'repository' - id_suffix = 'repo' - name_prefix = 'Repository for Koji' + id_suffix = 'repo' + name_prefix = 'Repository for Koji' for dep in self.deps: if isinstance(dep, (int, long)): # dep is a task ID, the url points to the task output directory @@ -1032,11 +1030,11 @@ class BaseBuildTask(BaseTaskHandler): tag_arches = [koji.canonArch(a) for a in tag['arches'].split()] host_arches = hostdata['arches'].split() if not set(tag_arches).intersection(host_arches): - self.logger.info('Task %s (%s): tag arches (%s) and ' \ - 'host arches (%s) are disjoint' % \ - (self.id, self.method, - ', '.join(tag_arches), ', '.join(host_arches))) - return False + self.logger.info('Task %s (%s): tag arches (%s) and ' \ + 'host arches (%s) are disjoint' % \ + (self.id, self.method, + ', '.join(tag_arches), ', '.join(host_arches))) + return False #otherwise... # This is in principle an error condition, but this is not a good place # to fail. Instead we proceed and let the task fail normally. @@ -1283,7 +1281,17 @@ class BuildMavenTask(BaseBuildTask): dirnames.remove(skip) for filename in filenames: filepath = os.path.join(dirpath, filename) - zfo.write(filepath, filepath[roottrim:]) + if os.path.islink(filepath): + content = os.readlink(filepath) + st = os.lstat(filepath) + mtime = time.localtime(st.st_mtime) + info = zipfile.ZipInfo(filepath[roottrim:]) + info.external_attr |= 0120000 << 16L # symlink file type + info.compress_type = zipfile.ZIP_STORED + info.date_time = mtime[:6] + zfo.writestr(info, content) + else: + zfo.write(filepath, filepath[roottrim:]) zfo.close() def checkHost(self, hostdata): @@ -2389,7 +2397,7 @@ class ImageTask(BaseTaskHandler): def fetchKickstart(self, broot, ksfile): """ - Retrieve the kickstart file we were given (locally or remotely) and + Retrieve the kickstart file we were given (locally or remotely) and upload it. Note that if the KS file existed locally, then "ksfile" is a relative @@ -2488,7 +2496,7 @@ class ImageTask(BaseTaskHandler): # Write out the new ks file. Note that things may not be in the same # order and comments in the original ks file may be lost. - kskoji = os.path.join('/tmp', 'koji-image-%s-%i.ks' % + kskoji = os.path.join('/tmp', 'koji-image-%s-%i.ks' % (target_info['build_tag_name'], self.id)) kojikspath = os.path.join(broot.rootdir(), kskoji[1:]) outfile = open(kojikspath, 'w') @@ -2503,7 +2511,7 @@ class ImageTask(BaseTaskHandler): def getImagePackages(self, cachepath): """ - Read RPM header information from the yum cache available in the + Read RPM header information from the yum cache available in the given path. Returns a list of dictionaries for each RPM included. """ found = False @@ -2524,8 +2532,8 @@ class ImageTask(BaseTaskHandler): raise koji.LiveCDError, 'No repos found in yum cache!' return hdrlist -# ApplianceTask begins with a mock chroot, and then installs appliance-tools -# into it via the appliance-build group. appliance-creator is then executed +# ApplianceTask begins with a mock chroot, and then installs appliance-tools +# into it via the appliance-build group. appliance-creator is then executed # in the chroot to create the appliance image. # class ApplianceTask(ImageTask): @@ -2548,7 +2556,7 @@ class ApplianceTask(ImageTask): if opts == None: opts = {} self.opts = opts - broot = self.makeImgBuildRoot(build_tag, repo_info, arch, + broot = self.makeImgBuildRoot(build_tag, repo_info, arch, 'appliance-build') kspath = self.fetchKickstart(broot, ksfile) self.readKickstart(kspath, opts) @@ -2561,11 +2569,11 @@ class ApplianceTask(ImageTask): app_log = '/tmp/appliance.log' os.mkdir(opath) - cmd = ['/usr/bin/appliance-creator', '-c', kskoji, '-d', '-v', + cmd = ['/usr/bin/appliance-creator', '-c', kskoji, '-d', '-v', '--logfile', app_log, '--cache', cachedir, '-o', odir] for arg_name in ('vmem', 'vcpu', 'format'): arg = opts.get(arg_name) - if arg != None: + if arg != None: cmd.extend(['--%s' % arg_name, arg]) appname = '%s-%s-%s' % (name, version, release) cmd.extend(['--name', appname]) @@ -2582,7 +2590,7 @@ class ApplianceTask(ImageTask): results = [] for directory, subdirs, files in os.walk(opath): for f in files: - results.append(os.path.join(broot.rootdir(), 'tmp', + results.append(os.path.join(broot.rootdir(), 'tmp', directory, f)) self.logger.debug('output: %s' % results) if len(results) == 0: @@ -2610,7 +2618,7 @@ class ApplianceTask(ImageTask): cachedir[1:])) broot.markExternalRPMs(hdrlist) imgdata['rpmlist'] = hdrlist - + broot.expire() return imgdata @@ -2706,7 +2714,7 @@ class LiveCDTask(ImageTask): livecd_log = '/tmp/livecd.log' cmd = ['/usr/bin/livecd-creator', '-c', kskoji, '-d', '-v', '--logfile', livecd_log, '--cache', cachedir] - # we set the fs label to the same as the isoname if it exists, + # we set the fs label to the same as the isoname if it exists, # taking at most 32 characters isoname = '%s-%s-%s' % (name, version, release) cmd.extend(['-f', isoname[:32]]) @@ -2833,7 +2841,7 @@ class OzImageTask(BaseTaskHandler): "'%s' : %s" % (kspath, e)) return ks - def prepareKickstart(self, kspath): + def prepareKickstart(self, kspath, install_tree): """ Process the ks file to be used for controlled image generation. This method also uploads the modified kickstart file to the task output @@ -2870,6 +2878,8 @@ class OzImageTask(BaseTaskHandler): self.logger.debug('BASEURL: %s' % baseurl) ks.handler.repo.repoList.append(repo_class( baseurl=baseurl, name='koji-override-0')) + # inject the URL of the install tree into the kickstart + ks.handler.url.url = install_tree return ks def writeKickstart(self, ksobj, ksname): @@ -3051,7 +3061,7 @@ class BaseImageTask(OzImageTask): Some image formats require others to be processed first, which is why we have to do this. raw files in particular may not be kept. """ - supported = ('raw', 'raw-xz', 'vmdk', 'qcow', 'qcow2', 'vdi', 'rhevm-ova', 'vsphere-ova', 'docker') + supported = ('raw', 'raw-xz', 'vmdk', 'qcow', 'qcow2', 'vdi', 'rhevm-ova', 'vsphere-ova', 'docker', 'vagrant-virtualbox', 'vagrant-libvirt', 'vpc') for f in formats: if f not in supported: raise koji.ApplianceError('Invalid format: %s' % f) @@ -3081,8 +3091,11 @@ class BaseImageTask(OzImageTask): 'vdi': self._buildConvert, 'qcow': self._buildConvert, 'qcow2': self._buildConvert, + 'vpc': self._buildConvert, 'rhevm-ova': self._buildOVA, 'vsphere-ova': self._buildOVA, + 'vagrant-virtualbox': self._buildOVA, + 'vagrant-libvirt': self._buildOVA, 'docker': self._buildDocker } # add a handler to the logger so that we capture ImageFactory's logging @@ -3221,6 +3234,17 @@ class BaseImageTask(OzImageTask): img_opts = {} if self.opts.get('ova_option'): img_opts = dict([o.split('=') for o in self.opts.get('ova_option')]) + # As far as Image Factory is concerned, vagrant boxes are just another type of OVA + # We communicate the desire for vagrant-specific formatting by adding the *_ova_format + # options and turning the underlying format option back into one of the two target + # image types ('vsphere-ova' or 'rhevm-ova') that are used to generate the intermediate + # disk image + if format == 'vagrant-virtualbox': + format = 'vsphere-ova' + img_opts['vsphere_ova_format'] = 'vagrant-virtualbox' + if format == 'vagrant-libvirt': + format = 'rhevm-ova' + img_opts['rhevm_ova_format'] = 'vagrant-libvirt' targ = self._do_target_image(self.base_img.base_image.identifier, format.replace('-ova', '')) targ2 = self._do_target_image(targ.target_image.identifier, 'OVA', @@ -3278,7 +3302,11 @@ class BaseImageTask(OzImageTask): @returns a dict with some metadata about the image """ - newimg = os.path.join(self.workdir, self.imgname + '.%s' % format) + self.logger.debug('converting an image to "%s"' % format) + ofmt = format + if format == 'vpc': + ofmt = 'vhd' + newimg = os.path.join(self.workdir, self.imgname + '.%s' % ofmt) cmd = ['/usr/bin/qemu-img', 'convert', '-f', 'raw', '-O', format, self.base_img.base_image.data, newimg] if format in ('qcow', 'qcow2'): @@ -3301,7 +3329,7 @@ class BaseImageTask(OzImageTask): # First, prepare the kickstart to use the repos we tell it kspath = self.fetchKickstart() - ks = self.prepareKickstart(kspath) + ks = self.prepareKickstart(kspath, inst_tree) kskoji = self.writeKickstart(ks, os.path.join(self.workdir, 'koji-%s-%i-base.ks' % (self.target_info['build_tag_name'], self.id))) @@ -3381,8 +3409,15 @@ class BaseImageTask(OzImageTask): newimg = images[format]['image'] if 'ova' in format or format == 'raw-xz': newname = self.imgname + '.' + format.replace('-', '.') + elif 'vagrant' in format: + # This embeds the vagrant target and the ".box" format in the name + # Previously, based on filename, these looked like OVAs + # This was confusing to many people + newname = self.imgname + '.' + format + '.box' elif format == 'docker': newname = self.imgname + '.' + 'tar.xz' + elif format == 'vpc': + newname = self.imgname + '.' + 'vhd' else: newname = self.imgname + '.' + format if format != 'docker': @@ -3398,7 +3433,7 @@ class BaseImageTask(OzImageTask): class BuildIndirectionImageTask(OzImageTask): Methods = ['indirectionimage'] - # So, these are copied directly from the base image class + # So, these are copied directly from the base image class # Realistically, we want to inherit methods from both BuildImageTask # and OzImageTask. # TODO: refactor - my initial suggestion would be to have OzImageTask @@ -3427,7 +3462,7 @@ class BuildIndirectionImageTask(OzImageTask): def fetchHubOrSCM(self, filepath, fileurl): """ Retrieve a file either from the hub or a remote scm - + If fileurl is None we assume we are being asked to retrieve from the hub and that filepath is relative to /mnt/koji/work. if fileurl contains a value we assume a remote SCM. @@ -3466,7 +3501,7 @@ class BuildIndirectionImageTask(OzImageTask): def _task_to_image(task_id): """ Take a task ID and turn it into an Image Factory Base Image object """ pim = PersistentImageManager.default_manager() - taskinfo = self.session.getTaskInfo(task_id) + taskinfo = self.session.getTaskInfo(task_id) taskstate = koji.TASK_STATES[taskinfo['state']].lower() if taskstate != 'closed': raise koji.BuildError("Input task (%d) must be in closed state - current state is (%s)" % @@ -3475,21 +3510,21 @@ class BuildIndirectionImageTask(OzImageTask): if taskmethod != "createImage": raise koji.BuildError("Input task method must be 'createImage' - actual method (%s)" % (taskmethod)) - result = self.session.getTaskResult(task_id) - files = self.session.listTaskOutput(task_id) + result = self.session.getTaskResult(task_id) + files = self.session.listTaskOutput(task_id) # This approach works for both scratch and saved/formal images # The downside is that we depend on the output file naming convention - def _match_name(inlist, namere): - for filename in inlist: - if re.search(namere, filename): - return filename - task_diskimage = _match_name(result['files'], ".*qcow2$") - task_tdl = _match_name(result['files'], "tdl.*xml") + def _match_name(inlist, namere): + for filename in inlist: + if re.search(namere, filename): + return filename + task_diskimage = _match_name(result['files'], ".*qcow2$") + task_tdl = _match_name(result['files'], "tdl.*xml") - task_dir = os.path.join(koji.pathinfo.work(),koji.pathinfo.taskrelpath(task_id)) - diskimage_full = os.path.join(task_dir, task_diskimage) - tdl_full = os.path.join(task_dir, task_tdl) + task_dir = os.path.join(koji.pathinfo.work(),koji.pathinfo.taskrelpath(task_id)) + diskimage_full = os.path.join(task_dir, task_diskimage) + tdl_full = os.path.join(task_dir, task_tdl) if not (os.path.isfile(diskimage_full) and os.path.isfile(tdl_full)): raise koji.BuildError("Missing TDL or qcow2 image for task (%d) - possible expired scratch build" % (task_id)) @@ -3508,7 +3543,7 @@ class BuildIndirectionImageTask(OzImageTask): factory_base_image.status = 'COMPLETE' # Now save it pim.save_image(factory_base_image) - + # We can now reference this object directly or via its UUID in persistent storage return factory_base_image @@ -3518,10 +3553,10 @@ class BuildIndirectionImageTask(OzImageTask): build = self.session.getBuild(nvr) if not build: raise koji.BuildError("Could not find build for (%s)" % (nvr)) - + buildarchives = self.session.listArchives(build['id']) if not buildarchives: - raise koji.Builderror("Could not retrieve archives for build (%s) from NVR (%s)" % + raise koji.Builderror("Could not retrieve archives for build (%s) from NVR (%s)" % (build['id'], nvr)) buildfiles = [ x['filename'] for x in buildarchives ] @@ -3539,7 +3574,7 @@ class BuildIndirectionImageTask(OzImageTask): tdl_full = os.path.join(builddir, build_tdl) if not (os.path.isfile(diskimage_full) and os.path.isfile(tdl_full)): - raise koji.BuildError("Missing TDL (%s) or qcow2 (%s) image for image (%s) - this should never happen" % + raise koji.BuildError("Missing TDL (%s) or qcow2 (%s) image for image (%s) - this should never happen" % (build_tdl, build_diskimage, nvr)) # The sequence to recreate a valid persistent image is as follows @@ -3588,12 +3623,12 @@ class BuildIndirectionImageTask(OzImageTask): release = opts['release'] # TODO: Another mostly copy-paste - if not release: - release = self.getRelease(name, version) - if '-' in version: - raise koji.ApplianceError('The Version may not have a hyphen') - if '-' in release: - raise koji.ApplianceError('The Release may not have a hyphen') + if not release: + release = self.getRelease(name, version) + if '-' in version: + raise koji.ApplianceError('The Version may not have a hyphen') + if '-' in release: + raise koji.ApplianceError('The Release may not have a hyphen') indirection_template = self.fetchHubOrSCM(opts.get('indirection_template'), opts.get('indirection_template_url')) @@ -3601,24 +3636,24 @@ class BuildIndirectionImageTask(OzImageTask): self.logger.debug('Got indirection template %s' % (indirection_template)) try: - if opts['utility_image_build']: - utility_factory_image = _nvr_to_image(opts['utility_image_build'], opts['arch']) - else: - utility_factory_image = _task_to_image(int(opts['utility_image_task'])) + if opts['utility_image_build']: + utility_factory_image = _nvr_to_image(opts['utility_image_build'], opts['arch']) + else: + utility_factory_image = _task_to_image(int(opts['utility_image_task'])) - if opts['base_image_build']: - base_factory_image = _nvr_to_image(opts['base_image_build'], opts['arch']) - else: - base_factory_image = _task_to_image(int(opts['base_image_task'])) + if opts['base_image_build']: + base_factory_image = _nvr_to_image(opts['base_image_build'], opts['arch']) + else: + base_factory_image = _task_to_image(int(opts['base_image_task'])) except Exception, e: self.logger.exception(e) raise # OK - We have a template and two input images - lets build - bld_info = None - if not opts['scratch']: - bld_info = self.initImageBuild(name, version, release, - target_info, opts) + bld_info = None + if not opts['scratch']: + bld_info = self.initImageBuild(name, version, release, + target_info, opts) try: return self._do_indirection(opts, base_factory_image, utility_factory_image, @@ -3667,9 +3702,9 @@ class BuildIndirectionImageTask(OzImageTask): open(target_image.data, "w").write("Mock build from task ID: %s" % (str(self.id))) target_image.status='COMPLETE' - else: + else: target = bd.builder_for_target_image('indirection', - image_id=base_factory_image.identifier, + image_id=base_factory_image.identifier, parameters=params) target.target_thread.join() except Exception, e: @@ -3697,7 +3732,7 @@ class BuildIndirectionImageTask(OzImageTask): myresults['logs'] = [ os.path.basename(ozlog) ] myresults['arch'] = opts['arch'] # TODO: This should instead track the two input images: base and utility - myresults['rpmlist'] = [ ] + myresults['rpmlist'] = [ ] # This is compatible with some helper methods originally implemented for the base # image build. In the original usage, the dict contains an entry per build arch @@ -3825,7 +3860,7 @@ class BuildSRPMFromSCMTask(BaseBuildTask): raise koji.BuildError, "Multiple srpms found in %s: %s" % (sourcedir, ", ".join(srpms)) else: srpm = srpms[0] - + # check srpm name h = koji.get_rpm_header(srpm) name = h[rpm.RPMTAG_NAME] @@ -3844,7 +3879,7 @@ class BuildSRPMFromSCMTask(BaseBuildTask): broot.expire() return {'srpm': "%s/%s" % (uploadpath, srpm_name), - 'logs': ["%s/%s" % (uploadpath, os.path.basename(f)) + 'logs': ["%s/%s" % (uploadpath, os.path.basename(f)) for f in log_files], 'brootid': brootid, } @@ -3969,16 +4004,16 @@ Build Info: %(weburl)s/buildinfo?buildID=%(build_id)i\r if not data: data = {} taskinfo = self.session.getTaskInfo(task_id) - + if not taskinfo: # invalid task_id return data - + if taskinfo['host_id']: hostinfo = self.session.getHost(taskinfo['host_id']) else: hostinfo = None - + result = None try: result = self.session.getTaskResult(task_id) @@ -3994,17 +4029,17 @@ Build Info: %(weburl)s/buildinfo?buildID=%(build_id)i\r sys.exc_clear() if not result: result = 'Unknown' - + files = self.session.listTaskOutput(task_id) logs = [filename for filename in files if filename.endswith('.log')] rpms = [filename for filename in files if filename.endswith('.rpm') and not filename.endswith('.src.rpm')] srpms = [filename for filename in files if filename.endswith('.src.rpm')] misc = [filename for filename in files if filename not in logs + rpms + srpms] - + logs.sort() rpms.sort() misc.sort() - + data[task_id] = {} data[task_id]['id'] = taskinfo['id'] data[task_id]['method'] = taskinfo['method'] @@ -4018,7 +4053,7 @@ Build Info: %(weburl)s/buildinfo?buildID=%(build_id)i\r data[task_id]['rpms'] = rpms data[task_id]['srpms'] = srpms data[task_id]['misc'] = misc - + children = self.session.getTaskChildren(task_id) for child in children: data = self._getTaskData(child['id'], data) @@ -4057,7 +4092,7 @@ Build Info: %(weburl)s/buildinfo?buildID=%(build_id)i\r elif build['state'] == koji.BUILD_STATES['FAILED']: failure_data = task_data[task_id]['result'] failed_hosts = ['%s (%s)' % (task['host'], task['arch']) for task in task_data.values() if task['host'] and task['state'] == 'failed'] - failure_info = "\r\n%s (%d) failed on %s:\r\n %s" % (build_nvr, build_id, + failure_info = "\r\n%s (%d) failed on %s:\r\n %s" % (build_nvr, build_id, ', '.join(failed_hosts), failure_data) @@ -4066,14 +4101,14 @@ Build Info: %(weburl)s/buildinfo?buildID=%(build_id)i\r tasks = {'failed' : [task for task in task_data.values() if task['state'] == 'failed'], 'canceled' : [task for task in task_data.values() if task['state'] == 'canceled'], 'closed' : [task for task in task_data.values() if task['state'] == 'closed']} - + srpms = [] for taskinfo in task_data.values(): for srpmfile in taskinfo['srpms']: srpms.append(srpmfile) srpms = self.uniq(srpms) srpms.sort() - + if srpms: output = "SRPMS:\r\n" for srpm in srpms: @@ -4564,7 +4599,7 @@ if __name__ == "__main__": except koji.AuthError, e: quit("Error: Unable to log in: %s" % e) except xmlrpclib.ProtocolError: - quit("Error: Unable to connect to server %s" % (options.server)) + quit("Error: Unable to connect to server %s" % (options.server)) elif options.user: try: # authenticate using user/password diff --git a/builder/kojid.service b/builder/kojid.service new file mode 100644 index 00000000..1886a440 --- /dev/null +++ b/builder/kojid.service @@ -0,0 +1,14 @@ +[Unit] +Description=Koji build server +Documentation=https://fedoraproject.org/wiki/Koji/ServerHowTo + +After=network.target + +[Service] +ExecStart=/usr/sbin/kojid \ + --fg \ + --force-lock \ + --verbose + +[Install] +WantedBy=multi-user.target diff --git a/cli/koji b/cli/koji index bfb0955c..908d829e 100755 --- a/cli/koji +++ b/cli/koji @@ -6,7 +6,7 @@ # # Koji is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; +# License as published by the Free Software Foundation; # version 2.1 of the License. # # This software is distributed in the hope that it will be useful, @@ -141,7 +141,7 @@ def get_options(): parser.add_option("--debug-xmlrpc", action="store_true", default=False, help=_("show xmlrpc debug output")) parser.add_option("-q", "--quiet", action="store_true", default=False, - help=_("run quietly")) + help=_("run quietly")) parser.add_option("--skip-main", action="store_true", default=False, help=_("don't actually run main")) parser.add_option("-s", "--server", help=_("url of XMLRPC server")) @@ -205,7 +205,7 @@ def get_options(): 'krbservice': 'host', 'cert': '~/.koji/client.crt', 'ca': '~/.koji/clientca.crt', - 'serverca': '~/.koji/serverca.crt', + 'serverca': '~/.koji/serverca.crt', 'authtype': None } #note: later config files override earlier ones @@ -263,7 +263,7 @@ def get_options(): # expand paths here, so we don't have to worry about it later value = os.path.expanduser(getattr(options, name)) setattr(options, name, value) - + #honor topdir if options.topdir: koji.BASEDIR = options.topdir @@ -427,7 +427,7 @@ def display_task_results(tasks): for task in [task for task in tasks.values() if task.level == 0]: state = task.info['state'] task_label = task.str() - + if state == koji.TASK_STATES['CLOSED']: print '%s completed successfully' % task_label elif state == koji.TASK_STATES['FAILED']: @@ -526,7 +526,7 @@ def watch_logs(session, tasklist, opts): while contents: if not taskoffsets.has_key(log): taskoffsets[log] = 0 - + contents = session.downloadTaskOutput(task_id, log, taskoffsets[log], 16384) taskoffsets[log] += len(contents) if contents: @@ -556,7 +556,7 @@ def handle_add_group(options, session, args): assert False tag = args[0] group = args[1] - + activate_session(session) if not session.hasPerm('admin'): print "This action requires admin privileges" @@ -566,7 +566,7 @@ def handle_add_group(options, session, args): if not dsttag: print "Unknown tag: %s" % tag return 1 - + groups = dict([(p['name'], p['group_id']) for p in session.getTagGroups(tag, inherit=False)]) group_id = groups.get(group, None) if group_id is not None: @@ -574,7 +574,7 @@ def handle_add_group(options, session, args): return 1 session.groupListAdd(tag, group) - + def handle_add_host(options, session, args): "[admin] Add a host" usage = _("usage: %prog add-host [options] hostname arch [arch2 ...]") @@ -694,7 +694,7 @@ def handle_remove_host_from_channel(options, session, args): if channel not in hostchannels: print "Host %s is not a member of channel %s" % (host, channel) return 1 - + session.removeHostFromChannel(host, channel) def handle_remove_channel(options, session, args): @@ -903,7 +903,7 @@ def handle_build(options, session, args): parser.error(_("Exactly two arguments (a build target and a SCM URL or srpm file) are required")) assert False if build_opts.arch_override and not build_opts.scratch: - parser.error(_("--arch_override is only allowed for --scratch builds")) + parser.error(_("--arch_override is only allowed for --scratch builds")) activate_session(session) target = args[0] if target.lower() == "none" and build_opts.repo_id: @@ -987,7 +987,7 @@ def handle_chain_build(options, session, args): return 1 sources = args[1:] - + src_list = [] build_level = [] #src_lists is a list of lists of sources to build. @@ -1017,7 +1017,7 @@ def handle_chain_build(options, session, args): if build_opts.background: #relative to koji.PRIO_DEFAULT priority = 5 - + task_id = session.chainBuild(src_list, target, priority=priority) print "Created task:", task_id @@ -2534,7 +2534,7 @@ def anon_handle_latest_build(options, session, args): print "%-40s %-20s %s" % ("Build","Tag","Built by") print "%s %s %s" % ("-"*40, "-"*20, "-"*16) options.quiet = True - + output = [ fmt % x for x in data] output.sort() for line in output: @@ -2927,7 +2927,7 @@ def anon_handle_list_hosts(options, session, args): for host in hosts: session.getLastHostUpdate(host['id']) updateList = session.multiCall() - + for host, [update] in zip(hosts, updateList): if update is None: host['update'] = '-' @@ -3166,12 +3166,12 @@ def handle_clone_tag(options, session, args): activate_session(session) if not session.hasPerm('admin') and not options.test: - print "This action requires admin privileges" - return + print "This action requires admin privileges" + return if args[0] == args[1]: sys.stdout.write('Source and destination tags must be different.\n') - return + return # store tags. srctag = session.getTag(args[0]) dsttag = session.getTag(args[1]) @@ -3433,7 +3433,7 @@ def handle_add_target(options, session, args): if not session.hasPerm('admin'): print "This action requires admin privileges" return 1 - + chkbuildtag = session.getTag(build_tag) chkdesttag = session.getTag(dest_tag) if not chkbuildtag: @@ -3445,7 +3445,7 @@ def handle_add_target(options, session, args): if not chkdesttag: print "Destination tag does not exist: %s" % dest_tag return 1 - + session.createBuildTarget(name, build_tag, dest_tag) def handle_edit_target(options, session, args): @@ -3509,13 +3509,13 @@ def handle_remove_target(options, session, args): if not session.hasPerm('admin'): print "This action requires admin privileges" return - + target = args[0] target_info = session.getBuildTarget(target) if not target_info: print "Build target %s does not exist" % target return 1 - + session.deleteBuildTarget(target_info['id']) def handle_remove_tag(options, session, args): @@ -4680,7 +4680,7 @@ def handle_edit_tag_inheritance(options, session, args): return 1 print _("Error: Key constraints may be broken. Exiting.") return 1 - + # len(data) == 1 data = data[0] @@ -5101,7 +5101,7 @@ def handle_image_build_indirection(options, session, args): usage += _("\n %prog image-build --config FILE") usage += _("\n\n(Specify the --help global option for a list of other " + "help options)") - parser = OptionParser(usage=usage) + parser = OptionParser(usage=usage) parser.add_option("--config", help=_("Use a configuration file to define image-build options " + "instead of command line options (they will be ignored).")) @@ -5151,19 +5151,19 @@ def _build_image_indirection(options, task_opts, session, args): """ # Do some sanity checks before even attempting to create the session - if not (bool(task_opts.utility_image_task) != + if not (bool(task_opts.utility_image_task) != bool(task_opts.utility_image_build)): raise koji.GenericError, _("You must specify either a utility-image task or build ID/NVR") - if not (bool(task_opts.base_image_task) != + if not (bool(task_opts.base_image_task) != bool(task_opts.base_image_build)): raise koji.GenericError, _("You must specify either a base-image task or build ID/NVR") required_opts = [ 'name', 'version', 'arch', 'target', 'indirection_template', 'results_loc' ] optional_opts = [ 'indirection_template_url', 'scratch', 'utility_image_task', 'utility_image_build', - 'base_image_task', 'base_image_build', 'release', 'skip_tag' ] + 'base_image_task', 'base_image_build', 'release', 'skip_tag' ] - missing = [ ] + missing = [ ] for opt in required_opts: if not getattr(task_opts, opt, None): missing.append(opt) @@ -5238,11 +5238,10 @@ def _build_image_indirection(options, task_opts, session, args): # return - - def handle_image_build(options, session, args): """Create a disk image given an install tree""" - formats = ('vmdk', 'qcow', 'qcow2', 'vdi', 'rhevm-ova', 'vsphere-ova', + formats = ('vmdk', 'qcow', 'qcow2', 'vdi', 'vpc', 'rhevm-ova', + 'vsphere-ova', 'vagrant-virtualbox', 'vagrant-libvirt', 'docker', 'raw-xz') usage = _("usage: %prog image-build [options] " + " [...]") @@ -5909,7 +5908,7 @@ def handle_move_build(opts, session, args): activate_session(session) tasks = [] builds = [] - + if options.all: for arg in args[2:]: pkg = session.getPackage(arg) @@ -5923,10 +5922,10 @@ def handle_move_build(opts, session, args): build = session.getBuild(arg) if not build: print _("Invalid build %s, skipping." % arg) - continue + continue if not build in builds: builds.append(build) - + for build in builds: task_id = session.moveBuild(args[0], args[1], build['id'], options.force) tasks.append(task_id) @@ -6045,7 +6044,7 @@ def anon_handle_download_build(options, session, args): elif len(args) > 1: parser.error(_("Only a single package N-V-R or build ID may be specified")) assert False - + activate_session(session) build = args[0] @@ -6060,7 +6059,7 @@ def anon_handle_download_build(options, session, args): print "No associated builds for task %s" % build return 1 build = builds[0]['build_id'] - + if suboptions.latestfrom: # We want the latest build, not a specific build try: @@ -6074,7 +6073,7 @@ def anon_handle_download_build(options, session, args): info = builds[0] else: info = session.getBuild(build) - + if info is None: print "No such build: %s" % build return 1 @@ -6134,7 +6133,7 @@ def anon_handle_download_build(options, session, args): pg = None else: pg = progress.TextMeter() - + for url, relpath in urls: file = grabber.urlopen(url, progress_obj=pg, text=relpath) @@ -6443,6 +6442,77 @@ def handle_moshimoshi(options, session, args): if u.get("krb_principal", None) is not None: print "Authenticated via Kerberos principal %s" % (u["krb_principal"]) + +def handle_runroot(options, session, args): + "[admin] Run a command in a buildroot" + usage = _("usage: %prog runroot [options] ") + usage += _("\n(Specify the --help global option for a list of other help options)") + parser = OptionParser(usage=usage) + parser.disable_interspersed_args() + parser.add_option("-p", "--package", action="append", default=[], help=_("make sure this package is in the chroot")) + parser.add_option("-m", "--mount", action="append", default=[], help=_("mount this directory read-write in the chroot")) + parser.add_option("--skip-setarch", action="store_true", default=False, + help=_("bypass normal setarch in the chroot")) + parser.add_option("-w", "--weight", type='int', help=_("set task weight")) + parser.add_option("--channel-override", help=_("use a non-standard channel")) + parser.add_option("--task-id", action="store_true", default=False, + help=_("Print the ID of the runroot task")) + parser.add_option("--use-shell", action="store_true", default=False, + help=_("Run command through a shell, otherwise uses exec")) + parser.add_option("--repo-id", type="int", help=_("ID of the repo to use")) + + (opts, args) = parser.parse_args(args) + + if len(args) < 3: + parser.error(_("Incorrect number of arguments")) + activate_session(session) + tag = args[0] + arch = args[1] + if opts.use_shell: + # everything must be correctly quoted + command = ' '.join(args[2:]) + else: + command = args[2:] + try: + task_id = session.runroot(tag, arch, command, + channel=opts.channel_override, + packages=opts.package, mounts=opts.mount, + repo_id=opts.repo_id, + skip_setarch=opts.skip_setarch, + weight=opts.weight) + except koji.GenericError as e: + if 'Invalid method' in str(e): + print "* The runroot plugin appears to not be installed on the", + print "koji hub. Please contact the administrator." + raise + if opts.task_id: + print task_id + + try: + while True: + # wait for the task to finish + if session.taskFinished(task_id): + break + time.sleep(options.poll_interval) + except KeyboardInterrupt: + # this is probably the right thing to do here + print "User interrupt: canceling runroot task" + session.cancelTask(task_id) + return + output = None + if "runroot.log" in session.listTaskOutput(task_id): + output = session.downloadTaskOutput(task_id, "runroot.log") + if output: + sys.stdout.write(output) + info = session.getTaskInfo(task_id) + if info is None: + sys.exit(1) + state = koji.TASK_STATES[info['state']] + if state in ('FAILED', 'CANCELED'): + sys.exit(1) + return + + def handle_help(options, session, args): "List available commands" usage = _("usage: %prog help [options]") diff --git a/docs/schema.sql b/docs/schema.sql index 9f7ab2e2..12b9f39f 100644 --- a/docs/schema.sql +++ b/docs/schema.sql @@ -190,6 +190,7 @@ INSERT INTO channels (name) VALUES ('maven'); INSERT INTO channels (name) VALUES ('livecd'); INSERT INTO channels (name) VALUES ('appliance'); INSERT INTO channels (name) VALUES ('vm'); +INSERT INTO channels (name) VALUES ('image'); -- Here we track the build machines -- each host has an entry in the users table also @@ -788,6 +789,8 @@ insert into archivetypes (name, description, extensions) values ('so', 'Shared l insert into archivetypes (name, description, extensions) values ('txt', 'Text file', 'txt'); insert into archivetypes (name, description, extensions) values ('vhd', 'Hyper-V image', 'vhd'); insert into archivetypes (name, description, extensions) values ('wsf', 'Windows script file', 'wsf'); +insert into archivetypes (name, description, extensions) values ('box', 'Vagrant Box Image', 'box'); +insert into archivetypes (name, description, extensions) values ('raw-xz', 'xz compressed raw disk image', 'raw.xz'); -- Do we want to enforce a constraint that a build can only generate one diff --git a/hub/httpd.conf b/hub/httpd.conf index 5f480ea1..de3f19f0 100644 --- a/hub/httpd.conf +++ b/hub/httpd.conf @@ -29,7 +29,10 @@ Alias /kojihub /usr/share/koji-hub/kojixmlrpc.py Alias /kojifiles "/mnt/koji/" - Options Indexes + Options Indexes SymLinksIfOwnerMatch + #If your top /mnt/koji directory is not owned by the httpd user, then + #you will need to follow all symlinks instead, e.g. + #Options Indexes FollowSymLinks AllowOverride None Require all granted #If you have httpd <= 2.2, you'll want the following two lines instead diff --git a/hub/kojihub.py b/hub/kojihub.py index 2c0eebd5..32169005 100644 --- a/hub/kojihub.py +++ b/hub/kojihub.py @@ -5,7 +5,7 @@ # # Koji is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; +# License as published by the Free Software Foundation; # version 2.1 of the License. # # This software is distributed in the hope that it will be useful, @@ -41,7 +41,6 @@ from koji.util import md5_constructor from koji.util import sha1_constructor from koji.util import dslice import os -import random import re import rpm import shutil @@ -253,7 +252,7 @@ class Task(object): self.runCallbacks('postTaskStateChange', info, 'priority', priority) if recurse: - """Change priority of child tasks""" + # Change priority of child tasks q = """SELECT id FROM task WHERE parent = %(task_id)s""" for (child_id,) in _fetchMulti(q, locals()): Task(child_id).setPriority(priority, recurse=True) @@ -709,8 +708,12 @@ def writeInheritanceData(tag_id, changes, clear=False): insert.make_create() insert.execute() -def readFullInheritance(tag_id,event=None,reverse=False,stops={},jumps={}): +def readFullInheritance(tag_id,event=None,reverse=False,stops=None,jumps=None): """Returns a list representing the full, ordered inheritance from tag""" + if stops is None: + stops = {} + if jumps is None: + jumps = {} order = [] readFullInheritanceRecurse(tag_id,event,order,stops,{},{},0,None,False,[],reverse,jumps) return order @@ -3449,8 +3452,8 @@ def list_rpms(buildID=None, buildrootID=None, imageID=None, componentBuildrootID # image specific constraints if imageID != None: - clauses.append('image_listing.image_id = %(imageID)i') - joins.append('image_listing ON rpminfo.id = image_listing.rpm_id') + clauses.append('image_listing.image_id = %(imageID)i') + joins.append('image_listing ON rpminfo.id = image_listing.rpm_id') if hostID != None: joins.append('standard_buildroot ON rpminfo.buildroot_id = standard_buildroot.id') @@ -3560,7 +3563,7 @@ def list_archives(buildID=None, buildrootID=None, imageID=None, componentBuildro checksum_type: the checksum type (integer) If componentBuildrootID is specified, then the map will also contain the following key: - project: whether the archive was pulled in as a project dependency, or as part of the + project: whether the archive was pulled in as a project dependency, or as part of the build environment setup (boolean) If 'type' is specified, then the archives listed will be limited @@ -3597,7 +3600,7 @@ def list_archives(buildID=None, buildrootID=None, imageID=None, componentBuildro an empty list is returned. """ values = {} - + tables = ['archiveinfo'] joins = ['archivetypes on archiveinfo.type_id = archivetypes.id'] fields = [('archiveinfo.id', 'id'), @@ -3906,8 +3909,8 @@ def get_archive_file(archive_id, filename): for file_info in files: if file_info['name'] == filename: return file_info - else: - return None + #otherwise + return None def list_task_output(taskID, stat=False): """List the files generated by the task with the given ID. This @@ -4735,6 +4738,119 @@ def _import_wrapper(task_id, build_info, rpm_results): import_build_log(os.path.join(rpm_task_dir, log), build_info, subdir='noarch') +def merge_scratch(task_id): + """Import rpms from a scratch build into an existing build, retaining + buildroot metadata and build logs.""" + task = Task(task_id) + try: + task_info = task.getInfo(request=True) + except koji.GenericError: + raise koji.ImportError, 'invalid task: %s' % task_id + if task_info['state'] != koji.TASK_STATES['CLOSED']: + raise koji.ImportError, 'task %s did not complete successfully' % task_id + if task_info['method'] != 'build': + raise koji.ImportError, 'task %s is not a build task' % task_id + if len(task_info['request']) < 3 or not task_info['request'][2].get('scratch'): + raise koji.ImportError, 'task %s is not a scratch build' % task_id + + # sanity check the task, and extract data required for import + srpm = None + tasks = {} + for child in task.getChildren(): + if child['method'] != 'buildArch': + continue + info = {'rpms': [], + 'logs': []} + for output in list_task_output(child['id']): + if output.endswith('.src.rpm'): + srpm_name = os.path.basename(output) + if not srpm: + srpm = srpm_name + else: + if srpm != srpm_name: + raise koji.ImportError, 'task srpm names do not match: %s, %s' % \ + (srpm, srpm_name) + elif output.endswith('.noarch.rpm'): + continue + elif output.endswith('.rpm'): + rpminfo = koji.parse_NVRA(os.path.basename(output)) + if 'arch' not in info: + info['arch'] = rpminfo['arch'] + elif info['arch'] != rpminfo['arch']: + raise koji.ImportError, 'multiple arches generated by task %s: %s, %s' % \ + (child['id'], info['arch'], rpminfo['arch']) + info['rpms'].append(output) + elif output.endswith('.log'): + info['logs'].append(output) + if not info['rpms']: + continue + if not info['logs']: + raise koji.ImportError, 'task %s is missing logs' % child['id'] + buildroots = query_buildroots(taskID=child['id'], + queryOpts={'order': '-id', 'limit': 1}) + if not buildroots: + raise koji.ImportError, 'no buildroot associated with task %s' % child['id'] + info['buildroot_id'] = buildroots[0]['id'] + tasks[child['id']] = info + if not tasks: + raise koji.ImportError, 'nothing to do for task %s' % task_id + + # sanity check the build + build_nvr = koji.parse_NVRA(srpm) + build = get_build(build_nvr) + if not build: + raise koji.ImportError, 'no such build: %(name)s-%(version)s-%(release)s' % \ + build_nvr + if build['state'] != koji.BUILD_STATES['COMPLETE']: + raise koji.ImportError, '%s did not complete successfully' % build['nvr'] + if not build['task_id']: + raise koji.ImportError, 'no task for %s' % build['nvr'] + build_task_info = Task(build['task_id']).getInfo(request=True) + # Intentionally skip checking the build task state. + # There are cases where the build can be valid even though the task has failed, + # e.g. tagging failures. + + # compare the task and build and make sure they are compatible with importing + if task_info['request'][0] != build_task_info['request'][0]: + raise koji.ImportError, 'SCM URLs for the task and build do not match: %s, %s' % \ + (task_info['request'][0], build_task_info['request'][0]) + build_arches = set() + for rpm in list_rpms(buildID=build['id']): + if rpm['arch'] == 'src': + build_srpm = '%s.src.rpm' % rpm['nvr'] + if srpm != build_srpm: + raise koji.ImportError, 'task and build srpm names do not match: %s, %s' % \ + (srpm, build_srpm) + elif rpm['arch'] == 'noarch': + continue + else: + build_arches.add(rpm['arch']) + if not build_arches: + raise koji.ImportError, 'no arch-specific rpms found for %s' % build['nvr'] + task_arches = set([t['arch'] for t in tasks.values()]) + overlapping_arches = task_arches.intersection(build_arches) + if overlapping_arches: + raise koji.ImportError, 'task %s and %s produce rpms with the same arches: %s' % \ + (task_info['id'], build['nvr'], ', '.join(overlapping_arches)) + + # everything looks good, do the import + for task_id, info in tasks.items(): + taskpath = koji.pathinfo.task(task_id) + for filename in info['rpms']: + filepath = os.path.realpath(os.path.join(taskpath, filename)) + rpminfo = import_rpm(filepath, build, info['buildroot_id']) + import_rpm_file(filepath, build, rpminfo) + add_rpm_sig(rpminfo['id'], koji.rip_rpm_sighdr(filepath)) + for logname in info['logs']: + logpath = os.path.realpath(os.path.join(taskpath, logname)) + import_build_log(logpath, build, subdir=info['arch']) + + # flag tags whose content has changed, so relevant repos can be regen'ed + for tag in list_tags(build=build['id']): + set_tag_update(tag['id'], 'IMPORT') + + return build['id'] + def get_archive_types(): """Return a list of all supported archivetypes""" select = """SELECT id, name, description, extensions FROM archivetypes @@ -4779,11 +4895,11 @@ def get_archive_type(filename=None, type_name=None, type_id=None, strict=False): elif len(results) > 1: # this should never happen, and is a misconfiguration in the database raise koji.GenericError, 'multiple matches for file extension: %s' % ext + #otherwise + if strict: + raise koji.GenericError, 'unsupported file extension: %s' % ext else: - if strict: - raise koji.GenericError, 'unsupported file extension: %s' % ext - else: - return None + return None def new_maven_build(build, maven_info): """ @@ -4855,7 +4971,7 @@ def old_image_data(old_image_id): ret = query.executeOne() if not ret: - raise koji.GenericError, 'no old image with ID: %i' % imageID + raise koji.GenericError, 'no old image with ID: %i' % old_image_id return ret def check_old_image_files(old): @@ -5992,7 +6108,7 @@ def get_notification_recipients(build, tag_id, state): for this tag and the user who submitted the build. The list will not contain duplicates. """ - + clauses = [] if build: @@ -7413,7 +7529,7 @@ class RootExports(object): elif md5sum is None: verify = None else: - verify, digest = info + verify, digest = md5sum sum_cls = get_verify_class(verify) if offset != -1: if size is not None: @@ -7585,7 +7701,7 @@ class RootExports(object): """ Import an archive file and associate it with a build. The archive can be any non-rpm filetype supported by Koji. - + filepath: path to the archive file (relative to the Koji workdir) buildinfo: information about the build to associate the archive with May be a string (NVR), integer (buildID), or dict (containing keys: name, version, release) @@ -7603,7 +7719,7 @@ class RootExports(object): elif type == 'image': context.session.assertPerm('image-import') else: - koji.GenericError, 'unsupported archive type: %s' % type + raise koji.GenericError, 'unsupported archive type: %s' % type buildinfo = get_build(buildinfo, strict=True) fullpath = '%s/%s' % (koji.pathinfo.work(), filepath) import_archive(fullpath, buildinfo, type, typeInfo) @@ -7690,6 +7806,36 @@ class RootExports(object): for tag in list_tags(build=rpminfo['build_id']): set_tag_update(tag['id'], 'IMPORT') + def mergeScratch(self, task_id): + """Import the rpms generated by a scratch build, and associate + them with an existing build. + + To be eligible for import, the build must: + - be successfully completed + - contain at least one arch-specific rpm + + The task must: + - be a 'build' task + - be successfully completed + - use the exact same SCM URL as the build + - contain at least one arch-specific rpm + - have no overlap between the arches of the rpms it contains and + the rpms contained by the build + - contain a .src.rpm whose filename exactly matches the .src.rpm + of the build + + Only arch-specific rpms will be imported. noarch rpms and the src + rpm will be skipped. Build logs and buildroot metadata from the + scratch build will be imported along with the rpms. + + This is useful for bootstrapping a new arch. RPMs can be built + for the new arch using a scratch build and then merged into an + existing build, incrementally expanding arch coverage and avoiding + the need for a mass-rebuild to support the new arch. + """ + context.session.assertPerm('admin') + return merge_scratch(task_id) + def addExternalRPM(self, rpminfo, external_repo, strict=True): """Import an external RPM @@ -8272,7 +8418,11 @@ class RootExports(object): context.session.assertPerm('admin') return writeInheritanceData(tag,data,clear=clear) - def getFullInheritance(self,tag,event=None,reverse=False,stops={},jumps={}): + def getFullInheritance(self,tag,event=None,reverse=False,stops=None,jumps=None): + if stops is None: + stops = {} + if jumps is None: + jumps = {} if not isinstance(tag,int): #lookup tag id tag = get_tag_id(tag,strict=True) @@ -8600,7 +8750,7 @@ class RootExports(object): raise koji.GenericError, 'user already exists: %s' % username if krb_principal and get_user(krb_principal): raise koji.GenericError, 'user with this Kerberos principal already exists: %s' % krb_principal - + return context.session.createUser(username, status=status, krb_principal=krb_principal) def enableUser(self, username): @@ -8609,14 +8759,14 @@ class RootExports(object): if not user: raise koji.GenericError, 'unknown user: %s' % username set_user_status(user, koji.USER_STATUS['NORMAL']) - + def disableUser(self, username): """Disable logins by the specified user""" user = get_user(username) if not user: raise koji.GenericError, 'unknown user: %s' % username set_user_status(user, koji.USER_STATUS['BLOCKED']) - + #group management calls newGroup = staticmethod(new_group) addGroupMember = staticmethod(add_group_member) @@ -9266,11 +9416,11 @@ class RootExports(object): notificationUser = self.getUser(user_id) if not notificationUser: raise koji.GenericError, 'invalid user ID: %s' % user_id - + if not (notificationUser['id'] == currentUser['id'] or self.hasPerm('admin')): raise koji.GenericError, 'user %s cannot create notifications for user %s' % \ (currentUser['name'], notificationUser['name']) - + email = '%s@%s' % (notificationUser['name'], context.opts['EmailDomain']) insert = """INSERT INTO build_notifications (user_id, package_id, tag_id, success_only, email) @@ -9297,7 +9447,7 @@ class RootExports(object): _dml(delete, locals()) def _prepareSearchTerms(self, terms, matchType): - """Process the search terms before passing them to the database. + r"""Process the search terms before passing them to the database. If matchType is "glob", "_" will be replaced with "\_" (to match literal underscores), "?" will be replaced with "_", and "*" will be replaced with "%". If matchType is "regexp", no changes will be @@ -10137,7 +10287,7 @@ class HostExports(object): if len(poms) == 0: pass elif len(poms) == 1: - # This directory has a .pom file, so get the Maven group_id, + # This directory has a .pom file, so get the Maven group_id, # artifact_id, and version from it and associate those with # the artifacts in this directory pom_path = os.path.join(maven_task_dir, relpath, poms[0]) @@ -10202,7 +10352,7 @@ class HostExports(object): if not context.opts.get('EnableWin'): raise koji.GenericError, 'Windows support not enabled' else: - koji.GenericError, 'unsupported archive type: %s' % type + raise koji.GenericError, 'unsupported archive type: %s' % type import_archive(filepath, buildinfo, type, typeInfo) def importWrapperRPMs(self, task_id, build_id, rpm_results): @@ -10378,7 +10528,7 @@ class HostExports(object): def importImage(self, task_id, build_id, results): """ - Import a built image, populating the database with metadata and + Import a built image, populating the database with metadata and moving the image to its final location. """ for sub_results in results.values(): diff --git a/hub/kojixmlrpc.py b/hub/kojixmlrpc.py index 67cb8eea..efb99a60 100644 --- a/hub/kojixmlrpc.py +++ b/hub/kojixmlrpc.py @@ -3,7 +3,7 @@ # # Koji is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; +# License as published by the Free Software Foundation; # version 2.1 of the License. # # This software is distributed in the hope that it will be useful, @@ -192,7 +192,7 @@ class HandlerAccess(object): return self.__reg.get(__name)(*args, **kwargs) def get(self, name): - return self.__Reg.get(name) + return self.__reg.get(name) class ModXMLRPCRequestHandler(object): @@ -292,7 +292,7 @@ class ModXMLRPCRequestHandler(object): if context.opts.get('LockOut') and \ context.method not in ('login', 'krbLogin', 'sslLogin', 'logout') and \ not context.session.hasPerm('admin'): - raise koji.ServerOffline, "Server disabled for maintenance" + raise koji.ServerOffline, "Server disabled for maintenance" def _dispatch(self, method, params): func = self._get_handler(method) @@ -569,7 +569,7 @@ def get_policy(opts, plugins): if pname != test.policy: continue elif pname not in test.policy: - continue + continue # in case of name overlap, last one wins # hence plugins can override builtin tests merged[name] = test diff --git a/koji.spec b/koji.spec index c2b1af40..d1e41817 100644 --- a/koji.spec +++ b/koji.spec @@ -1,5 +1,12 @@ %{!?python_sitelib: %define python_sitelib %(%{__python} -c "from distutils.sysconfig import get_python_lib; print get_python_lib()")} +%if 0%{?fedora} >= 21 || 0%{?redhat} >= 7 +%global use_systemd 1 +%else +%global use_systemd 0 +%global install_opt TYPE=sysv +%endif + %define baserelease 1 #build with --define 'testbuild 1' to have a timestamp appended to release %if "x%{?testbuild}" == "x1" @@ -23,6 +30,10 @@ Requires: rpm-python Requires: pyOpenSSL Requires: python-urlgrabber BuildRequires: python +%if %{use_systemd} +BuildRequires: systemd +BuildRequires: pkgconfig +%endif %description Koji is a system for building and tracking RPMS. The base package @@ -63,11 +74,17 @@ License: LGPLv2 and GPLv2+ #mergerepos (from createrepo) is GPLv2+ Requires: %{name} = %{version}-%{release} Requires: mock >= 0.9.14 +Requires(pre): /usr/sbin/useradd +%if %{use_systemd} +Requires(post): systemd +Requires(preun): systemd +Requires(postun): systemd +%else Requires(post): /sbin/chkconfig Requires(post): /sbin/service Requires(preun): /sbin/chkconfig Requires(preun): /sbin/service -Requires(pre): /usr/sbin/useradd +%endif Requires: /usr/bin/cvs Requires: /usr/bin/svn Requires: /usr/bin/git @@ -90,10 +107,16 @@ Summary: Koji virtual machine management daemon Group: Applications/System License: LGPLv2 Requires: %{name} = %{version}-%{release} +%if %{use_systemd} +Requires(post): systemd +Requires(preun): systemd +Requires(postun): systemd +%else Requires(post): /sbin/chkconfig Requires(post): /sbin/service Requires(preun): /sbin/chkconfig Requires(preun): /sbin/service +%endif Requires: libvirt-python Requires: libxml2-python Requires: /usr/bin/virt-clone @@ -109,6 +132,11 @@ Group: Applications/Internet License: LGPLv2 Requires: postgresql-python Requires: %{name} = %{version}-%{release} +%if %{use_systemd} +Requires(post): systemd +Requires(preun): systemd +Requires(postun): systemd +%endif %description utils Utilities for the Koji system @@ -135,7 +163,7 @@ koji-web is a web UI to the Koji system. %install rm -rf $RPM_BUILD_ROOT -make DESTDIR=$RPM_BUILD_ROOT install +make DESTDIR=$RPM_BUILD_ROOT %{?install_opt} install %clean rm -rf $RPM_BUILD_ROOT @@ -168,8 +196,12 @@ rm -rf $RPM_BUILD_ROOT %files utils %defattr(-,root,root) %{_sbindir}/kojira +%if %{use_systemd} +%{_unitdir}/kojira.service +%else %{_initrddir}/kojira %config(noreplace) %{_sysconfdir}/sysconfig/kojira +%endif %dir %{_sysconfdir}/kojira %config(noreplace) %{_sysconfdir}/kojira/kojira.conf %{_sbindir}/koji-gc @@ -192,8 +224,12 @@ rm -rf $RPM_BUILD_ROOT %{_sbindir}/kojid %dir %{_libexecdir}/kojid %{_libexecdir}/kojid/mergerepos +%if %{use_systemd} +%{_unitdir}/kojid.service +%else %{_initrddir}/kojid %config(noreplace) %{_sysconfdir}/sysconfig/kojid +%endif %dir %{_sysconfdir}/kojid %config(noreplace) %{_sysconfdir}/kojid/kojid.conf %attr(-,kojibuilder,kojibuilder) %{_sysconfdir}/mock/koji @@ -201,6 +237,19 @@ rm -rf $RPM_BUILD_ROOT %pre builder /usr/sbin/useradd -r -s /bin/bash -G mock -d /builddir -M kojibuilder 2>/dev/null ||: +%if %{use_systemd} + +%post builder +%systemd_post kojid.service + +%preun builder +%systemd_preun kojid.service + +%postun builder +%systemd_postun kojid.service + +%else + %post builder /sbin/chkconfig --add kojid @@ -209,17 +258,35 @@ if [ $1 = 0 ]; then /sbin/service kojid stop &> /dev/null /sbin/chkconfig --del kojid fi +%endif %files vm %defattr(-,root,root) %{_sbindir}/kojivmd #dir %{_datadir}/kojivmd %{_datadir}/kojivmd/kojikamid +%if %{use_systemd} +%{_unitdir}/kojivmd.service +%else %{_initrddir}/kojivmd %config(noreplace) %{_sysconfdir}/sysconfig/kojivmd +%endif %dir %{_sysconfdir}/kojivmd %config(noreplace) %{_sysconfdir}/kojivmd/kojivmd.conf +%if %{use_systemd} + +%post vm +%systemd_post kojivmd.service + +%preun vm +%systemd_preun kojivmd.service + +%postun vm +%systemd_postun kojivmd.service + +%else + %post vm /sbin/chkconfig --add kojivmd @@ -228,7 +295,20 @@ if [ $1 = 0 ]; then /sbin/service kojivmd stop &> /dev/null /sbin/chkconfig --del kojivmd fi +%endif +%if %{use_systemd} + +%post utils +%systemd_post kojira.service + +%preun utils +%systemd_preun kojira.service + +%postun utils +%systemd_postun kojira.service + +%else %post utils /sbin/chkconfig --add kojira /sbin/service kojira condrestart &> /dev/null || : @@ -237,6 +317,7 @@ if [ $1 = 0 ]; then /sbin/service kojira stop &> /dev/null || : /sbin/chkconfig --del kojira fi +%endif %changelog * Mon Mar 24 2014 Mike McLean - 1.9.0-1 diff --git a/koji/__init__.py b/koji/__init__.py index 42b91fef..f3b34fcd 100644 --- a/koji/__init__.py +++ b/koji/__init__.py @@ -5,7 +5,7 @@ # # Koji is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; +# License as published by the Free Software Foundation; # version 2.1 of the License. # # This software is distributed in the hope that it will be useful, @@ -328,6 +328,9 @@ class ParameterError(GenericError): """Raised when an rpc call receives incorrect arguments""" faultCode = 1019 +class ImportError(GenericError): + """Raised when an import fails""" + faultCode = 1020 class MultiCallInProgress(object): """ @@ -992,7 +995,7 @@ def parse_pom(path=None, contents=None): raise GenericError, 'either a path to a pom file or the contents of a pom file must be specified' # A common problem is non-UTF8 characters in XML files, so we'll convert the string first - + contents = fixEncoding(contents) try: @@ -1953,7 +1956,7 @@ class ClientSession(object): if self.logger.isEnabledFor(logging.DEBUG): tb_str = ''.join(traceback.format_exception(*sys.exc_info())) self.logger.debug(tb_str) - self.logger.info("Try #%d for call %d (%s) failed: %s", tries, self.callnum, name, e) + self.logger.info("Try #%s for call %s (%s) failed: %s", tries, self.callnum, name, e) if tries > 1: # first retry is immediate, after that we honor retry_interval time.sleep(interval) @@ -2039,10 +2042,10 @@ class ClientSession(object): chk_opts['verify'] = 'adler32' result = self._callMethod('checkUpload', (path, name), chk_opts) if int(result['size']) != ofs: - raise koji.GenericError, "Uploaded file is wrong length: %s/%s, %s != %s" \ + raise GenericError, "Uploaded file is wrong length: %s/%s, %s != %s" \ % (path, name, result['sumlength'], ofs) if problems and result['hexdigest'] != full_chksum.hexdigest(): - raise koji.GenericError, "Uploaded file has wrong checksum: %s/%s, %s != %s" \ + raise GenericError, "Uploaded file has wrong checksum: %s/%s, %s != %s" \ % (path, name, result['hexdigest'], full_chksum.hexdigest()) self.logger.debug("Fast upload: %s complete. %i bytes in %.1f seconds", localfile, size, t2) diff --git a/koji/auth.py b/koji/auth.py index 8e51ec7b..d419d770 100644 --- a/koji/auth.py +++ b/koji/auth.py @@ -3,7 +3,7 @@ # # Koji is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; +# License as published by the Free Software Foundation; # version 2.1 of the License. # # This software is distributed in the hope that it will be useful, @@ -247,7 +247,7 @@ class Session(object): if not result: raise koji.AuthError, 'invalid user_id: %s' % user_id name, usertype, status = result - + if status != koji.USER_STATUS['NORMAL']: raise koji.AuthError, 'logins by %s are not allowed' % name @@ -394,7 +394,7 @@ class Session(object): raise koji.AuthError, '%s is not authorized to login other users' % client_dn else: username = client_name - + cursor = context.cnx.cursor() query = """SELECT id FROM users WHERE name = %(username)s""" @@ -596,7 +596,7 @@ class Session(object): """ if not name: raise koji.GenericError, 'a user must have a non-empty name' - + if usertype == None: usertype = koji.USERTYPES['NORMAL'] elif not koji.USERTYPES.get(usertype): @@ -606,7 +606,7 @@ class Session(object): status = koji.USER_STATUS['NORMAL'] elif not koji.USER_STATUS.get(status): raise koji.GenericError, 'invalid status: %s' % status - + cursor = context.cnx.cursor() select = """SELECT nextval('users_id_seq')""" cursor.execute(select, locals()) diff --git a/koji/context.py b/koji/context.py index f904bed7..b05e3a3c 100755 --- a/koji/context.py +++ b/koji/context.py @@ -3,7 +3,7 @@ # # Koji is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; +# License as published by the Free Software Foundation; # version 2.1 of the License. # # This software is distributed in the hope that it will be useful, diff --git a/koji/daemon.py b/koji/daemon.py index b9f3070b..0570faa5 100644 --- a/koji/daemon.py +++ b/koji/daemon.py @@ -4,7 +4,7 @@ # # Koji is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; +# License as published by the Free Software Foundation; # version 2.1 of the License. # # This software is distributed in the hope that it will be useful, @@ -266,7 +266,7 @@ class SCM(object): # check for validity: params should be empty, query may be empty, everything else should be populated if params : - raise koji.GenericError, 'Unable to parse SCM URL: %s . Param element %s should be empty.' % (self.url,param) + raise koji.GenericError, 'Unable to parse SCM URL: %s . Params element %s should be empty.' % (self.url, params) if not scheme : raise koji.GenericError, 'Unable to parse SCM URL: %s . Could not find the scheme element.' % self.url if not netloc : diff --git a/koji/db.py b/koji/db.py index f5f61043..dcd24610 100644 --- a/koji/db.py +++ b/koji/db.py @@ -5,7 +5,7 @@ # # Koji is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; +# License as published by the Free Software Foundation; # version 2.1 of the License. # # This software is distributed in the hope that it will be useful, diff --git a/koji/plugin.py b/koji/plugin.py index e189d1f3..cbb245e8 100644 --- a/koji/plugin.py +++ b/koji/plugin.py @@ -3,7 +3,7 @@ # # Koji is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; +# License as published by the Free Software Foundation; # version 2.1 of the License. # # This software is distributed in the hope that it will be useful, diff --git a/koji/policy.py b/koji/policy.py index 47e72371..653f4143 100644 --- a/koji/policy.py +++ b/koji/policy.py @@ -2,7 +2,7 @@ # # Koji is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; +# License as published by the Free Software Foundation; # version 2.1 of the License. # # This software is distributed in the hope that it will be useful, @@ -368,4 +368,3 @@ def findSimpleTests(namespace): ret.setdefault(name, value) #...so first test wins in case of name overlap return ret - diff --git a/koji/server.py b/koji/server.py index 7d9ed3ef..52f13f53 100644 --- a/koji/server.py +++ b/koji/server.py @@ -158,7 +158,7 @@ class WSGIWrapper(object): for chunk in result: if chunk and not self.set_headers: raise RuntimeError, "write() called before start_response()" - write(data) + write(chunk) if not req.bytes_sent: #application sent nothing back req.set_content_length(0) @@ -187,4 +187,3 @@ class InputWrapper(object): while line: yield line line = self.readline() - diff --git a/koji/ssl/SSLCommon.py b/koji/ssl/SSLCommon.py index 5cb722d8..0d3fb947 100644 --- a/koji/ssl/SSLCommon.py +++ b/koji/ssl/SSLCommon.py @@ -139,4 +139,3 @@ class PlgHTTPS(httplib.HTTP): def __init__(self, host='', port=None, ssl_context=None, strict=None, timeout=None): self._setup(self._connection_class(host, port, ssl_context, strict, timeout)) - diff --git a/koji/ssl/SSLConnection.py b/koji/ssl/SSLConnection.py index 1bb9e762..5a45095b 100644 --- a/koji/ssl/SSLConnection.py +++ b/koji/ssl/SSLConnection.py @@ -156,4 +156,3 @@ class PlgFileObject(socket._fileobject): self._sock.close() finally: self._sock = None - diff --git a/koji/ssl/XMLRPCServerProxy.py b/koji/ssl/XMLRPCServerProxy.py index 40b174d6..16de6195 100644 --- a/koji/ssl/XMLRPCServerProxy.py +++ b/koji/ssl/XMLRPCServerProxy.py @@ -176,4 +176,3 @@ if __name__ == '__main__': except KeyboardInterrupt: os._exit(0) print "All done. (%d timed out)" % tm.get() - diff --git a/koji/tasks.py b/koji/tasks.py index 46a70994..0d9a0030 100644 --- a/koji/tasks.py +++ b/koji/tasks.py @@ -4,7 +4,7 @@ # # Koji is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; +# License as published by the Free Software Foundation; # version 2.1 of the License. # # This software is distributed in the hope that it will be useful, diff --git a/koji/util.py b/koji/util.py index e7c95a7e..80e511f3 100644 --- a/koji/util.py +++ b/koji/util.py @@ -2,7 +2,7 @@ # # Koji is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; +# License as published by the Free Software Foundation; # version 2.1 of the License. # # This software is distributed in the hope that it will be useful, diff --git a/plugins/messagebus.py b/plugins/messagebus.py index a23bc949..3f3dc6f7 100644 --- a/plugins/messagebus.py +++ b/plugins/messagebus.py @@ -22,19 +22,19 @@ session = None target = None def connect_timeout(host, port, timeout): - for res in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM): - af, socktype, proto, canonname, sa = res - sock = socket.socket(af, socktype, proto) - sock.settimeout(timeout) - try: - sock.connect(sa) - break - except socket.error, msg: - sock.close() - else: - # If we got here then we couldn't connect (yet) - raise - return sock + for res in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM): + af, socktype, proto, canonname, sa = res + sock = socket.socket(af, socktype, proto) + sock.settimeout(timeout) + try: + sock.connect(sa) + break + except socket.error, msg: + sock.close() + else: + # If we got here then we couldn't connect (yet) + raise + return sock class tlstimeout(qpid.messaging.transports.tls): def __init__(self, conn, host, port): @@ -168,6 +168,8 @@ def get_message_headers(msgtype, *args, **kws): headers['tag'] = kws['tag']['name'] headers['package'] = kws['package']['name'] elif msgtype == 'TaskStateChange': + headers['id'] = kws['info']['id'] + headers['parent'] = kws['info']['parent'] headers['method'] = kws['info']['method'] headers['attribute'] = kws['attribute'] headers['old'] = kws['old'] diff --git a/plugins/runroot.py b/plugins/runroot.py index 427d483d..86b6892b 100644 --- a/plugins/runroot.py +++ b/plugins/runroot.py @@ -319,5 +319,3 @@ class RunRootTask(tasks.BaseTaskHandler): os.unlink(fn) except OSError: pass - - diff --git a/plugins/runroot_hub.py b/plugins/runroot_hub.py new file mode 100644 index 00000000..a666919d --- /dev/null +++ b/plugins/runroot_hub.py @@ -0,0 +1,61 @@ +#koji hub plugin +# There is a kojid plugin that goes with this hub plugin. The kojid builder +# plugin has a config file. This hub plugin has no config file. + + +from koji.context import context +from koji.plugin import export +import koji +import random +import sys + +#XXX - have to import kojihub for mktask +sys.path.insert(0, '/usr/share/koji-hub/') +from kojihub import mktask, get_tag, get_all_arches + +__all__ = ('runroot',) + + +def get_channel_arches(channel): + """determine arches available in channel""" + chan = context.handlers.call('getChannel', channel, strict=True) + ret = {} + for host in context.handlers.call('listHosts', channelID=chan['id'], enabled=True): + for a in host['arches'].split(): + ret[koji.canonArch(a)] = 1 + return ret + +@export +def runroot(tagInfo, arch, command, channel=None, **opts): + """ Create a runroot task """ + context.session.assertPerm('runroot') + taskopts = { + 'priority': 15, + 'arch': arch, + } + + taskopts['channel'] = channel or 'runroot' + + if arch == 'noarch': + #not all arches can generate a proper buildroot for all tags + tag = get_tag(tagInfo) + if not tag['arches']: + raise koji.GenericError, 'no arches defined for tag %s' % tag['name'] + + #get all known arches for the system + fullarches = get_all_arches() + + tagarches = tag['arches'].split() + + # If our tag can't do all arches, then we need to + # specify one of the arches it can do. + if set(fullarches) - set(tagarches): + chanarches = get_channel_arches(taskopts['channel']) + choices = [x for x in tagarches if x in chanarches] + if not choices: + raise koji.GenericError, 'no common arches for tag/channel: %s/%s' \ + % (tagInfo, taskopts['channel']) + taskopts['arch'] = koji.canonArch(random.choice(choices)) + + return mktask(taskopts,'runroot', tagInfo, arch, command, **opts) + diff --git a/util/Makefile b/util/Makefile index 79ac64fa..5ea8bded 100644 --- a/util/Makefile +++ b/util/Makefile @@ -1,4 +1,6 @@ BINFILES = kojira koji-gc koji-shadow +SYSTEMDSYSTEMUNITDIR = $(shell pkg-config systemd --variable=systemdsystemunitdir) +TYPE = systemd _default: @echo "nothing to make. try make install" @@ -6,7 +8,7 @@ _default: clean: rm -f *.o *.so *.pyc *~ -install: +_install: @if [ "$(DESTDIR)" = "" ]; then \ echo " "; \ echo "ERROR: A destdir is required"; \ @@ -15,12 +17,6 @@ install: mkdir -p $(DESTDIR)/usr/sbin install -p -m 755 $(BINFILES) $(DESTDIR)/usr/sbin - mkdir -p $(DESTDIR)/etc/rc.d/init.d - install -p -m 755 kojira.init $(DESTDIR)/etc/rc.d/init.d/kojira - - mkdir -p $(DESTDIR)/etc/sysconfig - install -p -m 644 kojira.sysconfig $(DESTDIR)/etc/sysconfig/kojira - mkdir -p $(DESTDIR)/etc/kojira install -p -m 644 kojira.conf $(DESTDIR)/etc/kojira/kojira.conf @@ -29,3 +25,16 @@ install: mkdir -p $(DESTDIR)/etc/koji-shadow install -p -m 644 koji-shadow.conf $(DESTDIR)/etc/koji-shadow/koji-shadow.conf + +install-systemd: _install + mkdir -p $(DESTDIR)$(SYSTEMDSYSTEMUNITDIR) + install -p -m 644 kojira.service $(DESTDIR)$(SYSTEMDSYSTEMUNITDIR) + +install-sysv: _install + mkdir -p $(DESTDIR)/etc/rc.d/init.d + install -p -m 755 kojira.init $(DESTDIR)/etc/rc.d/init.d/kojira + + mkdir -p $(DESTDIR)/etc/sysconfig + install -p -m 644 kojira.sysconfig $(DESTDIR)/etc/sysconfig/kojira + +install: install-$(TYPE) diff --git a/util/koji-gc b/util/koji-gc index 871c7f89..2d61aa49 100755 --- a/util/koji-gc +++ b/util/koji-gc @@ -957,4 +957,3 @@ if __name__ == "__main__": pass if not options.skip_main: sys.exit(rv) - diff --git a/util/koji-shadow b/util/koji-shadow index cdeef8cd..3b62776a 100755 --- a/util/koji-shadow +++ b/util/koji-shadow @@ -1328,4 +1328,3 @@ if __name__ == "__main__": except: pass sys.exit(rv) - diff --git a/util/kojira b/util/kojira index fe827bee..c18f63b6 100755 --- a/util/kojira +++ b/util/kojira @@ -5,7 +5,7 @@ # # Koji is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; +# License as published by the Free Software Foundation; # version 2.1 of the License. # # This software is distributed in the hope that it will be useful, @@ -130,7 +130,7 @@ class ManagedRepo(object): if not tag_info: tag_info = getTag(self.session, self.tag_id, self.event_id) if not tag_info: - self.logger.warn('Could not get info for tag %i, skipping delete of repo %i' % + self.logger.warn('Could not get info for tag %i, skipping delete of repo %i' % (self.tag_id, self.repo_id)) return False tag_name = tag_info['name'] diff --git a/util/kojira.service b/util/kojira.service new file mode 100644 index 00000000..beaea18a --- /dev/null +++ b/util/kojira.service @@ -0,0 +1,14 @@ +[Unit] +Description=Koji repo administration +Documentation=https://fedoraproject.org/wiki/Koji/ServerHowTo + +After=network.target + +[Service] +ExecStart=/usr/sbin/kojira \ + --fg \ + --force-lock \ + --verbose + +[Install] +WantedBy=multi-user.target diff --git a/vm/Makefile b/vm/Makefile index 1dcab17a..13e340cb 100644 --- a/vm/Makefile +++ b/vm/Makefile @@ -1,6 +1,7 @@ - BINFILES = kojivmd SHAREFILES = kojikamid +SYSTEMDSYSTEMUNITDIR = $(shell pkg-config systemd --variable=systemdsystemunitdir) +TYPE = systemd _default: @echo "nothing to make. try make install" @@ -11,7 +12,7 @@ clean: kojikamid: kojikamid.py bash fix_kojikamid.sh >kojikamid -install: kojikamid +_install: kojikamid @if [ "$(DESTDIR)" = "" ]; then \ echo " "; \ echo "ERROR: A destdir is required"; \ @@ -24,13 +25,18 @@ install: kojikamid mkdir -p $(DESTDIR)/usr/share/kojivmd install -p -m 644 $(SHAREFILES) $(DESTDIR)/usr/share/kojivmd + mkdir -p $(DESTDIR)/etc/kojivmd + install -p -m 644 kojivmd.conf $(DESTDIR)/etc/kojivmd/kojivmd.conf + +install-systemd: _install + mkdir -p $(DESTDIR)$(SYSTEMDSYSTEMUNITDIR) + install -p -m 644 kojivmd.service $(DESTDIR)$(SYSTEMDSYSTEMUNITDIR) + +install-sysv: _install mkdir -p $(DESTDIR)/etc/rc.d/init.d install -p -m 755 kojivmd.init $(DESTDIR)/etc/rc.d/init.d/kojivmd mkdir -p $(DESTDIR)/etc/sysconfig install -p -m 644 kojivmd.sysconfig $(DESTDIR)/etc/sysconfig/kojivmd - mkdir -p $(DESTDIR)/etc/kojivmd - install -p -m 644 kojivmd.conf $(DESTDIR)/etc/kojivmd/kojivmd.conf - - +install: install-$(TYPE) diff --git a/vm/kojikamid.py b/vm/kojikamid.py index c142267a..15c05700 100755 --- a/vm/kojikamid.py +++ b/vm/kojikamid.py @@ -6,7 +6,7 @@ # # Koji is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; +# License as published by the Free Software Foundation; # version 2.1 of the License. # # This software is distributed in the hope that it will be useful, @@ -276,7 +276,7 @@ class WindowsBuild(object): continue tokens = entry.split(':') filename = tokens[0] - for var in ('name', 'version', 'release'): + for var in ('name', 'version', 'release'): filename = filename.replace('$' + var, getattr(self, var)) metadata = {} metadata['platforms'] = tokens[1].split(',') @@ -589,7 +589,7 @@ def get_mgmt_server(): macaddr, gateway, MANAGER_PORT) server = xmlrpclib.ServerProxy('http://%s:%s/' % (gateway, MANAGER_PORT), allow_none=True) - # we would set a timeout on the socket here, but that is apparently not + # we would set a timeout on the socket here, but that is apparently not # supported by python/cygwin/Windows task_port = server.getPort(macaddr) logger.debug('found task-specific port %s', task_port) diff --git a/vm/kojivmd.service b/vm/kojivmd.service new file mode 100644 index 00000000..a417fdbe --- /dev/null +++ b/vm/kojivmd.service @@ -0,0 +1,14 @@ +[Unit] +Description=Koji vm build server +Documentation=https://fedoraproject.org/wiki/Koji/ServerHowTo + +After=network.target + +[Service] +ExecStart=/usr/sbin/kojivmd \ + --fg \ + --force-lock \ + --verbose + +[Install] +WantedBy=multi-user.target diff --git a/www/conf/web.conf b/www/conf/web.conf index 171349b0..38f0b619 100644 --- a/www/conf/web.conf +++ b/www/conf/web.conf @@ -24,3 +24,8 @@ LoginTimeout = 72 # Secret = CHANGE_ME LibPath = /usr/share/koji-web/lib + +# If set to True, then the footer will be included literally. +# If False, then the footer will be included as another Kid Template. +# Defaults to True +LiteralFooter = True diff --git a/www/kojiweb/buildinfo.chtml b/www/kojiweb/buildinfo.chtml index 65fc3bad..07c62fdf 100644 --- a/www/kojiweb/buildinfo.chtml +++ b/www/kojiweb/buildinfo.chtml @@ -45,7 +45,7 @@ #end if - Built by$build.owner_name + Built by$build.owner_name #set $stateName = $util.stateName($build.state) diff --git a/www/kojiweb/builds.chtml b/www/kojiweb/builds.chtml index f221ffa2..337c466a 100644 --- a/www/kojiweb/builds.chtml +++ b/www/kojiweb/builds.chtml @@ -134,7 +134,7 @@ #if $tag $build.tag_name #end if - $build.owner_name + $build.owner_name $util.formatTime($build.completion_time) #set $stateName = $util.stateName($build.state) $util.stateImage($build.state) diff --git a/www/kojiweb/includes/footer.chtml b/www/kojiweb/includes/footer.chtml index 8dad3102..8e220220 100644 --- a/www/kojiweb/includes/footer.chtml +++ b/www/kojiweb/includes/footer.chtml @@ -8,8 +8,12 @@ #set $localfooterpath=$util.themePath("extra-footer.html", local=True) #if os.path.exists($localfooterpath) +#if $literalFooter #set $localfooter="".join(open($localfooterpath).readlines()) -$localfooter +#$localfooter +#else +#include $localfooterpath +#end if #end if diff --git a/www/kojiweb/index.chtml b/www/kojiweb/index.chtml index f6312170..738256c3 100644 --- a/www/kojiweb/index.chtml +++ b/www/kojiweb/index.chtml @@ -22,7 +22,7 @@ $build.build_id $build.nvr #if not $user - $build.owner_name + $build.owner_name #end if $util.formatTime($build.completion_time) $util.stateImage($build.state) @@ -50,12 +50,13 @@ State #for $task in $tasks - + #set $scratch = $util.taskScratchClass($task) + #set $state = $util.taskState($task.state) $task.id $koji.taskLabel($task) #if not $user - + #if $task.owner_type == $koji.USERTYPES['HOST'] $task.owner_name #else diff --git a/www/kojiweb/index.py b/www/kojiweb/index.py index ff15a5f5..4be6131f 100644 --- a/www/kojiweb/index.py +++ b/www/kojiweb/index.py @@ -286,7 +286,7 @@ def index(environ, packageOrder='package_name', packageStart=None): if user: packages = kojiweb.util.paginateResults(server, values, 'listPackages', kw={'userID': user['id'], 'with_dups': True}, start=packageStart, dataName='packages', prefix='package', order=packageOrder, pageSize=10) - + notifs = server.getBuildNotifications(user['id']) notifs.sort(kojiweb.util.sortByKeyFunc('id')) # XXX Make this a multicall @@ -294,21 +294,21 @@ def index(environ, packageOrder='package_name', packageStart=None): notif['package'] = None if notif['package_id']: notif['package'] = server.getPackage(notif['package_id']) - + notif['tag'] = None if notif['tag_id']: notif['tag'] = server.getTag(notif['tag_id']) values['notifs'] = notifs - + values['user'] = user values['welcomeMessage'] = environ['koji.options']['KojiGreeting'] - + return _genHTML(environ, 'index.chtml') def notificationedit(environ, notificationID): server = _getServer(environ) _assertLogin(environ) - + notificationID = int(notificationID) notification = server.getBuildNotification(notificationID) if notification == None: @@ -399,7 +399,7 @@ def notificationcreate(environ): def notificationdelete(environ, notificationID): server = _getServer(environ) _assertLogin(environ) - + notificationID = int(notificationID) notification = server.getBuildNotification(notificationID) if not notification: @@ -491,7 +491,7 @@ def tasks(environ, owner=None, state='active', view='tree', method='all', hostID if view in ('tree', 'toplevel'): opts['parent'] = None - + if state == 'active': opts['state'] = [koji.TASK_STATES['FREE'], koji.TASK_STATES['OPEN'], koji.TASK_STATES['ASSIGNED']] elif state == 'all': @@ -531,7 +531,7 @@ def tasks(environ, owner=None, state='active', view='tree', method='all', hostID tasks = kojiweb.util.paginateMethod(server, values, 'listTasks', kw={'opts': opts}, start=start, dataName='tasks', prefix='task', order=order) - + if view == 'tree': server.multicall = True for task in tasks: @@ -577,7 +577,7 @@ def taskinfo(environ, taskID): values['parent'] = parent else: values['parent'] = None - + descendents = server.getTaskDescendents(task['id'], request=True) values['descendents'] = descendents @@ -641,7 +641,7 @@ def taskinfo(environ, taskID): values['wrapTask'] = wrapTask elif task['method'] == 'restartVerify': values['rtask'] = server.getTaskInfo(params[0], request=True) - + if task['state'] in (koji.TASK_STATES['CLOSED'], koji.TASK_STATES['FAILED']): try: result = server.getTaskResult(task['id']) @@ -688,7 +688,7 @@ def taskstatus(environ, taskID): def resubmittask(environ, taskID): server = _getServer(environ) _assertLogin(environ) - + taskID = int(taskID) newTaskID = server.resubmitTask(taskID) _redirect(environ, 'taskinfo?taskID=%i' % newTaskID) @@ -817,13 +817,13 @@ def packages(environ, tagID=None, userID=None, order='package_name', start=None, values['prefix'] = prefix inherited = int(inherited) values['inherited'] = inherited - + packages = kojiweb.util.paginateResults(server, values, 'listPackages', kw={'tagID': tagID, 'userID': userID, 'prefix': prefix, 'inherited': bool(inherited)}, start=start, dataName='packages', prefix='package', order=order) - + values['chars'] = _PREFIX_CHARS - + return _genHTML(environ, 'packages.chtml') def packageinfo(environ, packageID, tagOrder='name', tagStart=None, buildOrder='-completion_time', buildStart=None): @@ -840,7 +840,7 @@ def packageinfo(environ, packageID, tagOrder='name', tagStart=None, buildOrder=' values['package'] = package values['packageID'] = package['id'] - + tags = kojiweb.util.paginateMethod(server, values, 'listTags', kw={'package': package['id']}, start=tagStart, dataName='tags', prefix='tag', order=tagOrder) builds = kojiweb.util.paginateMethod(server, values, 'listBuilds', kw={'packageID': package['id']}, @@ -966,7 +966,7 @@ def tagedit(environ, tagID): params['maven_include_all'] = bool(form.has_key('maven_include_all')) server.editTag2(tag['id'], **params) - + _redirect(environ, 'taginfo?tagID=%i' % tag['id']) elif form.has_key('cancel'): _redirect(environ, 'taginfo?tagID=%i' % tag['id']) @@ -1016,7 +1016,7 @@ def tagparent(environ, tagID, parentID, action): data = server.getInheritanceData(tag['id']) data.append(newDatum) - + server.setInheritanceData(tag['id'], data) elif form.has_key('cancel'): pass @@ -1039,7 +1039,7 @@ def tagparent(environ, tagID, parentID, action): values['inheritanceData'] = inheritanceData[0] else: raise koji.GenericError, 'tag %i has tag %i listed as a parent more than once' % (tag['id'], parent['id']) - + return _genHTML(environ, 'tagparent.chtml') elif action == 'remove': data = server.getInheritanceData(tag['id']) @@ -1076,7 +1076,7 @@ def buildinfo(environ, buildID): server = _getServer(environ) buildID = int(buildID) - + build = server.getBuild(buildID) values['title'] = koji.buildLabel(build) + ' | Build Info' @@ -1170,7 +1170,7 @@ def buildinfo(environ, buildID): values['imagebuild'] = imagebuild values['archives'] = archives values['archivesByExt'] = archivesByExt - + values['noarch_log_dest'] = noarch_log_dest if environ['koji.currentUser']: values['perms'] = server.getUserPerms(environ['koji.currentUser']['id']) @@ -1243,7 +1243,7 @@ def builds(environ, userID=None, tagID=None, packageID=None, state=None, order=' if prefix not in _PREFIX_CHARS: prefix = None values['prefix'] = prefix - + values['order'] = order if type in ('maven', 'win', 'image'): pass @@ -1274,7 +1274,7 @@ def builds(environ, userID=None, tagID=None, packageID=None, state=None, order=' 'type': type, 'state': state, 'prefix': prefix}, start=start, dataName='builds', prefix='build', order=order) - + values['chars'] = _PREFIX_CHARS return _genHTML(environ, 'builds.chtml') @@ -1295,7 +1295,7 @@ def users(environ, order='name', start=None, prefix=None): start=start, dataName='users', prefix='user', order=order) values['chars'] = _PREFIX_CHARS - + return _genHTML(environ, 'users.chtml') def userinfo(environ, userID, packageOrder='package_name', packageStart=None, buildOrder='-completion_time', buildStart=None): @@ -1314,10 +1314,10 @@ def userinfo(environ, userID, packageOrder='package_name', packageStart=None, bu packages = kojiweb.util.paginateResults(server, values, 'listPackages', kw={'userID': user['id'], 'with_dups': True}, start=packageStart, dataName='packages', prefix='package', order=packageOrder, pageSize=10) - + builds = kojiweb.util.paginateMethod(server, values, 'listBuilds', kw={'userID': user['id']}, start=buildStart, dataName='builds', prefix='build', order=buildOrder, pageSize=10) - + return _genHTML(environ, 'userinfo.chtml') def rpminfo(environ, rpmID, fileOrder='name', fileStart=None, buildrootOrder='-id', buildrootStart=None): @@ -1360,7 +1360,7 @@ def rpminfo(environ, rpmID, fileOrder='name', fileStart=None, buildrootOrder='-i values['build'] = build values['builtInRoot'] = builtInRoot values['buildroots'] = buildroots - + files = kojiweb.util.paginateMethod(server, values, 'listRPMFiles', args=[rpm['id']], start=fileStart, dataName='files', prefix='file', order=fileOrder) @@ -1408,7 +1408,7 @@ def fileinfo(environ, filename, rpmID=None, archiveID=None): values['rpm'] = None values['archive'] = None - + if rpmID: rpmID = int(rpmID) rpm = server.getRPM(rpmID) @@ -1439,7 +1439,7 @@ def fileinfo(environ, filename, rpmID=None, archiveID=None): def cancelbuild(environ, buildID): server = _getServer(environ) _assertLogin(environ) - + buildID = int(buildID) build = server.getBuild(buildID) if build == None: @@ -1468,7 +1468,7 @@ def hosts(environ, state='enabled', start=None, order='name'): values['state'] = state hosts = server.listHosts(**args) - + server.multicall = True for host in hosts: server.getLastHostUpdate(host['id']) @@ -1518,7 +1518,7 @@ def hostinfo(environ, hostID=None, userID=None): values['perms'] = server.getUserPerms(environ['koji.currentUser']['id']) else: values['perms'] = [] - + return _genHTML(environ, 'hostinfo.chtml') def hostedit(environ, hostID): @@ -1630,7 +1630,7 @@ def buildrootinfo(environ, buildrootID, builtStart=None, builtOrder=None, compon values['buildroot'] = buildroot values['task'] = task - + return _genHTML(environ, 'buildrootinfo.chtml') def rpmlist(environ, type, buildrootID=None, imageID=None, start=None, order='nvr'): @@ -1714,13 +1714,13 @@ def buildtargets(environ, start=None, order='name'): targets = kojiweb.util.paginateMethod(server, values, 'getBuildTargets', start=start, dataName='targets', prefix='target', order=order) - + values['order'] = order if environ['koji.currentUser']: values['perms'] = server.getUserPerms(environ['koji.currentUser']['id']) else: values['perms'] = [] - + return _genHTML(environ, 'buildtargets.chtml') def buildtargetinfo(environ, targetID=None, name=None): @@ -1733,7 +1733,7 @@ def buildtargetinfo(environ, targetID=None, name=None): target = server.getBuildTarget(targetID) elif name != None: target = server.getBuildTarget(name) - + if target == None: raise koji.GenericError, 'invalid build target: %s' % (targetID or name) @@ -1785,7 +1785,7 @@ def buildtargetedit(environ, targetID): values = _initValues(environ, 'Edit Build Target', 'buildtargets') tags = server.listTags() tags.sort(_sortbyname) - + values['target'] = target values['tags'] = tags @@ -1810,7 +1810,7 @@ def buildtargetcreate(environ): if target == None: raise koji.GenericError, 'error creating build target "%s"' % name - + _redirect(environ, 'buildtargetinfo?targetID=%i' % target['id']) elif form.has_key('cancel'): _redirect(environ, 'buildtargets') @@ -1860,7 +1860,7 @@ def buildsbyuser(environ, start=None, order='-builds'): user['builds'] = numBuilds if numBuilds > maxBuilds: maxBuilds = numBuilds - + values['order'] = order graphWidth = 400.0 @@ -1893,14 +1893,14 @@ def rpmsbyhost(environ, start=None, order=None, hostArch=None, rpmArch=None): host['rpms'] = numRPMs if numRPMs > maxRPMs: maxRPMs = numRPMs - + values['hostArch'] = hostArch hostArchList = server.getAllArches() hostArchList.sort() values['hostArchList'] = hostArchList values['rpmArch'] = rpmArch values['rpmArchList'] = hostArchList + ['noarch', 'src'] - + if order == None: order = '-rpms' values['order'] = order @@ -1947,11 +1947,11 @@ def tasksbyhost(environ, start=None, order='-tasks', hostArch=None): server = _getServer(environ) maxTasks = 1 - + hostArchFilter = hostArch if hostArchFilter == 'ix86': hostArchFilter = ['i386', 'i486', 'i586', 'i686'] - + hosts = server.listHosts(arches=hostArchFilter) server.multicall = True @@ -1963,12 +1963,12 @@ def tasksbyhost(environ, start=None, order='-tasks', hostArch=None): host['tasks'] = numTasks if numTasks > maxTasks: maxTasks = numTasks - + values['hostArch'] = hostArch hostArchList = server.getAllArches() hostArchList.sort() values['hostArchList'] = hostArchList - + values['order'] = order graphWidth = 400.0 @@ -1984,7 +1984,7 @@ def tasksbyuser(environ, start=None, order='-tasks'): server = _getServer(environ) maxTasks = 1 - + users = server.listUsers() server.multicall = True @@ -1996,7 +1996,7 @@ def tasksbyuser(environ, start=None, order='-tasks'): user['tasks'] = numTasks if numTasks > maxTasks: maxTasks = numTasks - + values['order'] = order graphWidth = 400.0 @@ -2025,7 +2025,7 @@ def buildsbystatus(environ, days='7'): server.listBuilds(completeAfter=dateAfter, state=koji.BUILD_STATES['FAILED'], taskID=-1, queryOpts={'countOnly': True}) server.listBuilds(completeAfter=dateAfter, state=koji.BUILD_STATES['CANCELED'], taskID=-1, queryOpts={'countOnly': True}) [[numSucceeded], [numFailed], [numCanceled]] = server.multiCall() - + values['numSucceeded'] = numSucceeded values['numFailed'] = numFailed values['numCanceled'] = numCanceled @@ -2070,7 +2070,7 @@ def buildsbytarget(environ, days='7', start=None, order='-builds'): if builds > maxBuilds: maxBuilds = builds - kojiweb.util.paginateList(values, targets.values(), start, 'targets', 'target', order) + kojiweb.util.paginateList(values, targets.values(), start, 'targets', 'target', order) values['order'] = order @@ -2140,7 +2140,7 @@ def recentbuilds(environ, user=None, tag=None, package=None): task = None builds[i]['task'] = task builds[i]['changelog'] = clogs[i][0] - + values['tag'] = tagObj values['user'] = userObj values['package'] = packageObj @@ -2197,7 +2197,7 @@ def search(environ, start=None, order='name'): raise koji.GenericError, 'unknown search type: %s' % type values['infoURL'] = infoURL values['order'] = order - + results = kojiweb.util.paginateMethod(server, values, 'search', args=(terms, type, match), start=start, dataName='results', prefix='result', order=order) if not start and len(results) == 1: diff --git a/www/kojiweb/packageinfo.chtml b/www/kojiweb/packageinfo.chtml index e0fcb716..313bc88e 100644 --- a/www/kojiweb/packageinfo.chtml +++ b/www/kojiweb/packageinfo.chtml @@ -47,7 +47,7 @@ #for $build in $builds $build.nvr - $build.owner_name + $build.owner_name $util.formatTime($build.completion_time) #set $stateName = $util.stateName($build.state) $util.stateImage($build.state) diff --git a/www/kojiweb/packages.chtml b/www/kojiweb/packages.chtml index 6de2cf83..08cae840 100644 --- a/www/kojiweb/packages.chtml +++ b/www/kojiweb/packages.chtml @@ -78,7 +78,7 @@ $package.package_name #if $tag or $user $package.tag_name - $package.owner_name + $package.owner_name #if $package.blocked then $util.imageTag('no') else $util.imageTag('yes')# #end if diff --git a/www/kojiweb/tasks.chtml b/www/kojiweb/tasks.chtml index 897bfcc4..4c1a32fa 100644 --- a/www/kojiweb/tasks.chtml +++ b/www/kojiweb/tasks.chtml @@ -145,7 +145,7 @@ All $printChildren($task.id, $task.descendents) #end if - + #if $task.owner_type == $koji.USERTYPES['HOST'] $task.owner_name #else diff --git a/www/kojiweb/wsgi_publisher.py b/www/kojiweb/wsgi_publisher.py index 4f58b8e7..e7908152 100644 --- a/www/kojiweb/wsgi_publisher.py +++ b/www/kojiweb/wsgi_publisher.py @@ -69,6 +69,7 @@ class Dispatcher(object): ['KojiFilesURL', 'string', 'http://localhost/kojifiles'], ['KojiTheme', 'string', None], ['KojiGreeting', 'string', 'Welcome to Koji Web'], + ['LiteralFooter', 'boolean', True], ['WebPrincipal', 'string', None], ['WebKeytab', 'string', '/etc/httpd.keytab'], diff --git a/www/lib/kojiweb/util.py b/www/lib/kojiweb/util.py index ae007577..8156d799 100644 --- a/www/lib/kojiweb/util.py +++ b/www/lib/kojiweb/util.py @@ -54,6 +54,7 @@ def _initValues(environ, title='Build System Info', pageID='summary'): values['title'] = title values['pageID'] = pageID values['currentDate'] = str(datetime.datetime.now()) + values['literalFooter'] = environ['koji.options'].get('LiteralFooter', True) themeCache.clear() themeInfo.clear() themeInfo['name'] = environ['koji.options'].get('KojiTheme', None) @@ -225,7 +226,7 @@ def passthrough_except(template, *exclude): previously used #attr _PASSTHROUGH = ... to define the list of variable names to be passed-through. - Any variables names passed in will be excluded from the + Any variables names passed in will be excluded from the list of variables in the output string. """ passvars = [] @@ -245,7 +246,7 @@ def sortByKeyFunc(key, noneGreatest=False): cmpFunc = lambda a, b: (a is None or b is None) and -(cmp(a, b)) or cmp(a, b) else: cmpFunc = cmp - + if key.startswith('-'): key = key[1:] sortFunc = lambda a, b: cmpFunc(b[key], a[key]) @@ -265,7 +266,7 @@ def paginateList(values, data, start, dataName, prefix=None, order=None, noneGre """ if order != None: data.sort(sortByKeyFunc(order, noneGreatest)) - + totalRows = len(data) if start: @@ -277,7 +278,7 @@ def paginateList(values, data, start, dataName, prefix=None, order=None, noneGre count = len(data) _populateValues(values, dataName, prefix, data, totalRows, start, count, pageSize, order) - + return data def paginateMethod(server, values, methodName, args=None, kw=None, @@ -294,10 +295,10 @@ def paginateMethod(server, values, methodName, args=None, kw=None, start = 0 if not dataName: raise StandardError, 'dataName must be specified' - + kw['queryOpts'] = {'countOnly': True} totalRows = getattr(server, methodName)(*args, **kw) - + kw['queryOpts'] = {'order': order, 'offset': start, 'limit': pageSize} @@ -402,7 +403,7 @@ def formatDep(name, version, flags): a human-readable format. Copied from rpmUtils/miscutils.py:formatRequires()""" s = name - + if flags: if flags & (koji.RPMSENSE_LESS | koji.RPMSENSE_GREATER | koji.RPMSENSE_EQUAL): @@ -458,6 +459,19 @@ def rowToggle(template): else: return 'row-even' + +def taskScratchClass(task_object): + """ Return a css class indicating whether or not this task is a scratch + build. + """ + request = task_object['request'] + if len(request) >= 3: + opts = request[2] + if opts.get('scratch'): + return "scratch" + return "" + + _fileFlags = {1: 'configuration', 2: 'documentation', 4: 'icon', @@ -567,4 +581,3 @@ a network issue or load issues on the server.""" else: str = "An error has occurred while processing your request." return str, level -