Esempio n. 1
0
    def __init__(self, cfg, ui, rmakeCfgFn=None, conaryCfg=None, rmakeCfg=None):
        self._cfg = cfg
        self._ui = ui

        if conaryCfg:
            self._ccfg = conaryCfg
        else:
            self._ccfg = conarycfg.ConaryConfiguration(readConfigFiles=False)
            self._ccfg.read(util.join(self._cfg.configPath, 'conaryrc'))
            self._ccfg.dbPath = ':memory:'
            self._ccfg.initializeFlavors()

        self._client = conaryclient.ConaryClient(self._ccfg)

        if self._cfg.saveChangeSets or self._cfg.sanityCheckChangesets:
            self._saveChangeSets = tempfile.mkdtemp(
                prefix=self._cfg.platformName,
                suffix='-import-changesets')
        else:
            self._saveChangeSets = False

        self._sanityCheckChangesets = self._cfg.sanityCheckChangesets
        self._sanityCheckCommits = self._cfg.sanityCheckCommits

        if rmakeCfg:
            self._rmakeCfg = rmakeCfg
        else:
            self._rmakeCfg = self._getRmakeConfig(rmakeCfgFn=rmakeCfgFn)

        self._helper = helper.rMakeHelper(buildConfig=self._rmakeCfg)

        self.cvc = Cvc(self._cfg, self._ccfg, self._formatInput,
                       LocalDispatcher(self, 12))

        self._asyncDispatcher = OrderedCommitDispatcher(self, 30)

        self._conaryhelper = ConaryHelper(self._cfg)
Esempio n. 2
0
class Builder(object):
    """
    Class for wrapping the rMake api until we can switch to using rBuild.

    @param cfg: updateBot configuration object
    @type cfg: config.UpdateBotConfig
    @param ui: command line user interface.
    @type ui: cmdline.ui.UserInterface
    """

    def __init__(self, cfg, ui, rmakeCfgFn=None, conaryCfg=None, rmakeCfg=None):
        self._cfg = cfg
        self._ui = ui

        if conaryCfg:
            self._ccfg = conaryCfg
        else:
            self._ccfg = conarycfg.ConaryConfiguration(readConfigFiles=False)
            self._ccfg.read(util.join(self._cfg.configPath, 'conaryrc'))
            self._ccfg.dbPath = ':memory:'
            self._ccfg.initializeFlavors()

        self._client = conaryclient.ConaryClient(self._ccfg)

        if self._cfg.saveChangeSets or self._cfg.sanityCheckChangesets:
            self._saveChangeSets = tempfile.mkdtemp(
                prefix=self._cfg.platformName,
                suffix='-import-changesets')
        else:
            self._saveChangeSets = False

        self._sanityCheckChangesets = self._cfg.sanityCheckChangesets
        self._sanityCheckCommits = self._cfg.sanityCheckCommits

        if rmakeCfg:
            self._rmakeCfg = rmakeCfg
        else:
            self._rmakeCfg = self._getRmakeConfig(rmakeCfgFn=rmakeCfgFn)

        self._helper = helper.rMakeHelper(buildConfig=self._rmakeCfg)

        self.cvc = Cvc(self._cfg, self._ccfg, self._formatInput,
                       LocalDispatcher(self, 12))

        self._asyncDispatcher = OrderedCommitDispatcher(self, 30)

        self._conaryhelper = ConaryHelper(self._cfg)

    def _getRmakeConfig(self, rmakeCfgFn=None):
        # Get default pluginDirs from the rmake cfg object, setup the plugin
        # manager, then create a new rmake config object so that rmakeUser
        # will be parsed correctly.
        rmakeCfg = buildcfg.BuildConfiguration(readConfigFiles=False)
        disabledPlugins = [ x[0] for x in rmakeCfg.usePlugin.items()
                            if not x[1] ]
        disabledPlugins.append('monitor')
        pluginMgr = plugins.PluginManager(rmakeCfg.pluginDirs, disabledPlugins)
        pluginMgr.loadPlugins()
        pluginMgr.callClientHook('client_preInit', self, [])

        rmakerc = 'rmakerc'
        if rmakeCfgFn:
            rmakeCfgPath = util.join(self._cfg.configPath, rmakeCfgFn)
            if os.path.exists(rmakeCfgPath):
                rmakerc = rmakeCfgFn
                # FIXME: This is a hack to work around having two conaryrc
                #        files, one for building groups and one for building
                #        packages.
                self._ccfg.autoLoadRecipes = []
            else:
                log.warn('%s not found, falling back to rmakerc' % rmakeCfgFn)

        rmakeCfg = buildcfg.BuildConfiguration(readConfigFiles=False)
        rmakeCfg.read(util.join(self._cfg.configPath, rmakerc))
        rmakeCfg.useConaryConfig(self._ccfg)
        rmakeCfg.copyInConfig = False
        rmakeCfg.strictMode = True

        # Use default tmpDir when building with rMake since the specified
        # tmpDir may not exist in the build root.
        rmakeCfg.resetToDefault('tmpDir')

        return rmakeCfg

    def build(self, troveSpecs):
        """
        Build a list of troves.
        @param troveSpecs: list of trove specs
        @type troveSpecs: [(name, versionObj, flavorObj), ...]
        @return troveMap: dictionary of troveSpecs to built troves
        """

        if not troveSpecs:
            return {}

        troves = self._formatInput(troveSpecs)
        jobId = self._startJob(troves)
        self._monitorJob(jobId, retry=2)
        self._sanityCheckJob(jobId)
        trvMap = self._commitJob(jobId)
        ret = self._formatOutput(trvMap)
        return ret

    def buildmany(self, troveSpecs, lateCommit=False, workers=None,
            retries=None):
        """
        Build many troves in separate jobs.
        @param troveSpecs: list of trove specs
        @type troveSpecs: [(name, versionObj, flavorObj), ...]
        @param lateCommit: if True, build all troves, then commit. (defaults to
                           False)
        @type lateCommit: boolean
        @return troveMap: dictionary of troveSpecs to built troves
        """

        if not workers:
            workers = 30

        if not retries:
            retries = 0


        if self._cfg.updateMode == 'current':
            if self._cfg.targetLabel == self._cfg.sourceLabel[-1]:
                dispatcher = Dispatcher(self, workers, retries=retries)
            else:
                dispatcher = PromoteDispatcher(self, workers, retries=retries)
        elif not lateCommit:
            dispatcher = Dispatcher(self, workers, retries=retries)
        else:
            dispatcher = NonCommittalDispatcher(self, workers, retries=retries)
        return dispatcher.buildmany(troveSpecs)

    def buildsplitarch(self, troveSpecs):
        """
        Build a list of packages, in N jobs where N is the number of
        configured arch contexts.
        @param troveSpecs: list of trove specs
        @type troveSpecs: [(name, versionObj, flavorObj), ...]
        @return troveMap: dictionary of troveSpecs to built troves
        """

        if not troveSpecs:
            return {}

        # Split troves by context.
        jobs = {}
        for trv in self._formatInput(troveSpecs):
            if len(trv) != 4:
                continue

            key = trv[3]
            if key not in jobs:
                jobs[key] = []
            jobs[key].append(trv)

        # Start all build jobs.
        jobIds = {}
        for ctx, job in jobs.iteritems():
            jobIds[ctx] = self._startJob(job)

        fmtstr = ', '.join([ '%s:%s' % (x, y) for x, y in jobIds.iteritems()])
        log.info('Started %s' % fmtstr)

        # Wait for the jobs to finish.
        log.info('Waiting for jobs to complete')
        for jobId in jobIds.itervalues():
            self._monitorJob(jobId)

        # Sanity check all jobs.
        for jobId in jobIds.itervalues():
            self._sanityCheckJob(jobId)

        # Commit if all jobs were successfull.
        trvMap = self._commitJob(jobIds.values())

        ret = self._formatOutput(trvMap)
        return ret

    def buildasync(self, troveSpec):
        """
        Build troves in much the same way buildmany does, but without blocking
        the main thread.
        @param troveSpec: name, version, flavor tuple
        @type troveSpec: tuple(str, conary.versions.VersionFromString, None)
        @return status object
        @rtype updatebot.build.jobs.Status
        """

        return self._asyncDispatcher.build(troveSpec)

    def rebuild(self, troveSpecs, useLatest=None, additionalResolveTroves=None,
        commit=True):
        """
        Rebuild a set of troves in the same environment that they were
        orignally built in.
        @param troveSpecs: set of name, version, flavor tuples
        @type troveSpecs: set([(name, version, flavor), ..])
        @param useLatest: A list of package names to use the latest versions of.
                          For instance, you may want to use the latest version
                          of conary to get fixed dependencies.
        @type useLatest: list(str, ...)
        @param additionalResolveTroves: List of additional trove specs to add to
                                        the resolve troves.
        @type additionalResolveTroves: list(str, ...)
        @param commit: Controls waiting for jobs to complete and then committing
                       them one at a time. (default: True)
        @type commit: boolean
        @return if commit: troveMap: dictionary of troveSpecs to built troves
        @return if not commit: list of jobIds
        """

        # Set some defaults
        if useLatest is None:
            useLatest = []
        if additionalResolveTroves is None:
            additionalResolveTroves = []

        def grpByNameVersion(jobLst):
            lst = {}
            for job in jobLst:
                lst.setdefault(tuple(job[:2]), set()).add(job)

            return [ lst[x] for x in sorted(lst.keys()) ]

        def startOne(job):
            # Get a new builder so that we don't change the configuration of the
            # existing builder.
            cls = self.__class__
            builder = cls(self._cfg, self._ui)

            # Find the troves that were originally used to build the requested
            # trove.
            n, v = list(job)[0][:2]
            # So that we can find the latest binary built from the closest
            # versioned source we need to lookup binary versions and then use
            # that source to get the rest of the binaries. We need to do this
            # in the case that we are building a source that has been modified
            # to remove a recipe or something like that.
            upVer = '/'.join([v.branch().label().asString(),
                              v.trailingRevision().version])
            binSpecs = self._client.repos.findTrove(v.branch().label(),
                                                    (n, upVer, None))
            binTrv = self._client.repos.getTrove(*binSpecs[0], withFiles=False)
            srcName = binTrv.troveInfo.sourceName()
            srcVersion = binTrv.getVersion().getSourceVersion()

            specs = self._client.repos.getTrovesBySource(srcName, srcVersion)

            # Find the latest version of each package
            vMap = {}
            for n, v, f in specs:
                n = n.split(':')[0]
                vMap.setdefault(v, dict()).setdefault(n, set()).add(f)

            latest = sorted(vMap)[-1]
            trvSpecs = []
            for n, flvs in vMap[latest].iteritems():
                for f in flvs:
                    trvSpecs.append((n, latest, f))

            # Get the troves for all binaries built from the given source.
            troves = self._client.repos.getTroves(trvSpecs, withFiles=False)

            # Take the union of all buildreqs for all flavors of the package.
            reqs = set()
            for trv in troves:
                for req in trv.troveInfo.buildReqs.iter():
                    name = req.name()
                    version = req.version()
                    if useLatest and name.split(':')[0] in useLatest:
                        # XXX - this broke for me at some point, so I
                        # switched to using "continue" here; I've now
                        # forgotten the reason, and so changed it back. - AG
                        version = version.branch()
                    reqs.add((name, version))

            # Reconfigure builder to use previous buildrequires as
            # resolveTroves.
            resolveTroves = ' '.join([ '%s=%s' % x for x in reqs ])

            # Add any additional resolve troves.
            resolveTroves += ' ' + ' '.join(additionalResolveTroves)

            builder._rmakeCfg.resolveTroves = []
            builder._rmakeCfg.configLine('resolveTroves %s' % resolveTroves)

            # Start the job.
            jobId = builder._startJob(job)

            return jobId


        # Handle empty set.
        if not troveSpecs:
            return {}

        jobs = self._formatInput(troveSpecs)

        # Sanity check input to make sure there are no groups.
        if [ x for x in jobs if x[0].startswith('group-') ]:
            raise GroupBuildNotSupportedError

        # Start all of the jobs.
        jobIds = []
        for job in grpByNameVersion(jobs):
            jobIds.append(startOne(job))

        # If not committing jobs, return the list of ids.
        if not commit:
            return jobIds

        # Wait for jobs to complete
        dispatcher = NonCommittalDispatcher(self, 0)
        dispatcher.watchmany(jobIds)

        # Commit jobs in order, conary does not do this for you.
        trvMap = {}
        for jobId in jobIds:
            trvMap.update(self._commitJob(jobId))
        ret = self._formatOutput(trvMap)

        return ret

    def rebuildmany(self, troveSpecs, useLatest=None,
        additionalResolveTroves=None, commit=True):
        """
        Rebuild a set of troves in the same environment that they were
        orignally built in.
        @param troveSpecs: set of name, version, flavor tuples
        @type troveSpecs: set([(name, version, flavor), ..])
        @param useLatest: A list of package names to use the latest versions of.
                          For instance, you may want to use the latest version
                          of conary to get fixed dependencies.
        @type useLatest: list(str, ...)
        @param additionalResolveTroves: List of additional trove specs to add to
                                        the resolve troves.
        @type additionalResolveTroves: list(str, ...)
        @param commit: Controls waiting for jobs to complete and then committing
                       them one at a time. (default: True)
        @type commit: boolean
        @return if commit: troveMap: dictionary of troveSpecs to built troves
        @return if not commit: list of jobIds
        """

        dispatcher = RebuildDispatcher(self, 30, useLatest=useLatest,
            additionalResolveTroves=additionalResolveTroves)

        return dispatcher.buildmany(troveSpecs)


    def start(self, troveSpecs):
        """
        Public version of start job that starts a job without monitoring.
        @param troveSpecs: set of name, version, flavor tuples
        @type troveSpecs: set([(name, version, flavor), ..])
        @return jobId: integer
        """

        troves = self._formatInput(troveSpecs)
        jobId = self._startJob(troves)
        return jobId

    def watch(self, jobId):
        """
        Watch a build.
        @param jobId: rMake job ID
        @type jobId: integer
        """

        self._monitorJob(jobId)

    def commit(self, jobId):
        """
        Public commit from jobId with sanity checking.
        @param jobId: id of the build job to commit
        @type jobId: integer
        @return dict((name, version, flavor)=
                     set([(name, version, flavor), ...])
        """

        self._sanityCheckJob(jobId)
        trvMap = self._commitJob(jobId)
        ret = self._formatOutput(trvMap)
        return ret

    def orderJobs(self, troveSpecs):
        """
        Create a sorted list of troveSpecs, grouped according to the config.
        @param troveSpecs: list of source name, source version, and flavor.
        @type troveSpecs: iterable of three tuples
        """

        order = []

        bucketMap = {}
        for grouping in self._cfg.combinePackages:
            for pkg in grouping:
                bucketMap[pkg] = self._cfg.combinePackages.index(grouping)

        buckets = {}
        for nvf in sorted(troveSpecs):
            name = nvf[0]
            if name in bucketMap:
                if name not in buckets:
                    order.append([])
                    idx = len(order) - 1

                    for pkg in self._cfg.combinePackages[bucketMap[name]]:
                        buckets[name] = idx

                idx = buckets[name]
                order[idx].append(nvf)
            else:
                order.append(nvf)

        return order

    def setCommitFailed(self, jobId, reason=None):
        """
        Sets the job as failed in rmake.
        @param jobId: id of the build job to commit
        @type jobId: integer
        @param reason: message to be stored on the rmake server
        @type resaon: str
        """

        reason = reason and reason or 'none specified'
        self._helper.client.commitFailed([jobId, ], reason)

    def _formatInput(self, troveSpecs):
        """
        Formats the list of troves provided into a job list for rMake.
        @param troveSpecs: set of name, version, flavor tuples
        @type troveSpecs: set([(name, version, flavor, optional list of binary
                                names), ..])
        @return list((name, version, flavor, context), ...)
        """

        # Make sure troveSpecs is an iterable of three tuples.
        if (len(troveSpecs) in (3, 4) and
            not isinstance(list(troveSpecs)[0], (list, set, tuple))):
            # Assume that (n,v,f) was passed in
            troveSpecs = [ troveSpecs, ]

        # Build all troves in defined contexts.
        troves = []
        for trv in troveSpecs:
            if len(trv) == 3:
                name, version, flavor = trv
                binaryNames = None
            elif len(trv) == 4:
                name, version, flavor, binaryNames = trv
            else:
                raise InvalidBuildTroveInputError(input=trv)

            # Make sure name is not an unicode string, it causes breakage in
            # the deps modules in conary.
            name = name.encode()

            # Make sure name is not a component, like a source component.
            name = name.split(':')[0]

            # Build groups in all of the defined flavors. We don't need a
            # context here since groups are all built in a single job.
            if name.startswith('group-'):
                for flv in self._cfg.groupFlavors:
                    troves.append((name, version, flv))

            # Handle special package flavors when specified.
            elif name in self._cfg.packageFlavors:
                for context, flavor in self._cfg.packageFlavors[name]:
                    troves.append((name, version, flavor, context))

            # Kernels are special.
            elif ((name == 'kernel' or
                   name in self._cfg.kernelModules or
                   util.isKernelModulePackage(name))
                  and self._cfg.kernelFlavors):
                for context, flavor in self._cfg.kernelFlavors:
                    # Replace flag name to match package
                    if name != 'kernel':
                        flavor = deps.parseFlavor(
                            str(flavor).replace('kernel', name))
                    troves.append((name, version, flavor, context))

            # Check if this looks like a kernel module source rpm that wasn't
            # handled by the last two checks.
            elif '-kmod' in name or '-kmp' in name:
                log.error('raising error for kernel module package %s' % name)
                raise UnhandledKernelModule(name=name)

            # All other packages.
            else:
                # Build all packages as x86 and x86_64.
                for context, fltr in self._cfg.archContexts:
                    # If this is no filter or if there is a filter and a binary
                    # package matches the filter build in this context.
                    if (not fltr or (fltr and
                            [ x for x in binaryNames if fltr[1].match(x) ])):
                        troves.append((name, version, flavor, context))

            # Handle any special-case omissions
            # (e.g. due to missing packages)
            if name in self._cfg.packageFlavorsMissing:
                for context, flavor, fltr in self._cfg.packageFlavorsMissing[name]:
                    if not [ x for x in binaryNames if fltr[1].match(x) ]:
                        troves.remove((name, version, flavor, context))

            assert troves

        return sorted(set(troves))

    @jobInfoExceptionHandler
    def _getJob(self, jobId, retry=None):
        """
        Get a job instance from the rMake helper, catching several common
        exceptions.
        @param jobId: id of an rMake job
        @type jobId: integer
        @param retry: information about retrying the get job, if retry is None
                      then retry forever, if retry is an integer retry n times.
        @type retry: None
        @type retry: integer
        @return rmake job instance
        """

        if not isinstance(jobId, (list, tuple, set)):
            return self._helper.client.getJob(jobId)
        else:
            return self._helper.client.getJobs(jobId)

    def _startJob(self, troveSpecs):
        """
        Create and start a rMake build.
        @param troveSpecs: list of trove specs
        @type troveSpecs: [(name, versionObj, flavorObj), ...]
        @return integer jobId
        """

        # Create rMake job
        log.info('Creating build job: %s' % (troveSpecs, ))
        job = self._helper.createBuildJob(list(troveSpecs))
        jobId = self._helper.buildJob(job)
        log.info('Started jobId: %s' % jobId)

        return jobId

    @jobInfoExceptionHandler
    def _monitorJob(self, jobId):
        """
        Monitor job status, block until complete.
        @param jobId: rMake job ID
        @type jobId: integer
        """

        # Watch build, wait for completion
        monitor.monitorJob(self._helper.client, jobId,
            exitOnFinish=True, displayClass=StatusOnlyDisplay)

    def _sanityCheckJob(self, jobIds):
        """
        Verify the status of a job.
        @param jobIds: rMake job ID, or list of jobIds
        @type jobIds: integer or iterable
        """

        if not isinstance(jobIds, (tuple, list, set)):
            jobIds = [ jobIds, ]

        # Check for errors
        for job in self._getJob(jobIds):
            jobId = job.jobId
            if job.isFailed():
                log.error('Job %d failed', jobId)
                raise JobFailedError(jobId=jobId, why=job.status)
            elif not job.isFinished():
                log.error('Job %d is not done, yet watch returned early!',
                          jobId)
                raise JobFailedError(jobId=jobId, why=job.status)
            elif not list(job.iterBuiltTroves()):
                log.error('Job %d has no built troves', jobId)
                raise JobFailedError(jobId=jobId, why='No troves found in job')

    def _sanityCheckRPMCapsule(self, jobId, fileList, fileObjs, rpmFile):
        """
        Compare an rpm capsule with the contents of a
        trove to make sure that they agree.
        """

        rpmFile.seek(0)
        h = rpmhelper.readHeader(rpmFile, checkSize=False)
        rpmFileList = dict(
            itertools.izip(h[rpmhelper.OLDFILENAMES],
                           itertools.izip(h[rpmhelper.FILEUSERNAME],
                                          h[rpmhelper.FILEGROUPNAME],
                                          h[rpmhelper.FILEMODES],
                                          h[rpmhelper.FILESIZES],
                                          h[rpmhelper.FILERDEVS],
                                          h[rpmhelper.FILEFLAGS],
                                          h[rpmhelper.FILEVERIFYFLAGS],
                                          h[rpmhelper.FILELINKTOS],
                                          )))

        errors = []

        foundFiles = dict.fromkeys(rpmFileList)

        def fassert(test, path='', why=''):
            if not test:
                errors.append((path, why))

        def devassert(path, rDev, fileObj):
            minor = rDev & 0xff | (rDev >> 12) & 0xffffff00
            major = (rDev >> 8) & 0xfff
            fassert(fileObj.devt.major() == major, path,
                    'Device major mismatch: RPM %d != Conary %d'
                    %(major, fileObj.devt.major()))
            fassert(fileObj.devt.minor() == minor, path,
                    'Device minor mismatch: RPM %d != Conary %d'
                    %(minor, fileObj.devt.minor()))
            fassert(not fileObj.flags.isPayload(), path,
                    'Device file is marked as payload')

        for fileInfo, fileObj in zip(fileList, fileObjs):
            fpath = fileInfo[1]
            foundFiles[fpath] = True
            rUser, rGroup, rMode, rSize, rDev, rFlags, rVflags, rLinkto = \
                rpmFileList[fpath]

            # First, tests based on the Conary changeset

            # file metadata verification
            if (rUser != fileObj.inode.owner() or
                rGroup != fileObj.inode.group()):
                fassert(False, fpath,
                        'User/Group mismatch: RPM %s:%s != Conary %s:%s'
                        %(rUser, rGroup,
                          fileObj.inode.owner(), fileObj.inode.group()))

            if isinstance(fileObj, files.SymbolicLink):
                expectedMode = 0777 # CNY-3304
            else:
                expectedMode = stat.S_IMODE(rMode)
            if fileObj.inode.perms() != expectedMode:
                fassert(False, fpath,
                        'Mode mismatch: RPM 0%o != Conary 0%o'
                        %(expectedMode, fileObj.inode.perms()))

            if isinstance(fileObj, files.RegularFile):
                if not stat.S_ISREG(rMode):
                    fassert(False, fpath,
                            'Conary Regular file has non-regular mode 0%o'
                            %rMode)

                # RPM config flag mapping
                if rFlags & rpmhelper.RPMFILE_CONFIG:
                    if fileObj.linkGroup() or not fileObj.contents.size():
                        fassert(fileObj.flags.isInitialContents(), fpath,
                                'RPM config file without size or'
                                ' hardlinked is not InitialContents')
                    else:
                        fassert(fileObj.flags.isConfig() or
                                fileObj.flags.isInitialContents(), fpath,
                                'RPM config file is neither Config file '
                                'nor InitialContents')

            elif isinstance(fileObj, files.Directory):
                fassert(stat.S_ISDIR(rMode), fpath,
                        'Conary directory has non-directory RPM mode 0%o'
                        %rMode)
                fassert(not fileObj.flags.isPayload(), fpath,
                        'Conary directory marked as payload')

            elif isinstance(fileObj, files.CharacterDevice):
                fassert(stat.S_ISCHR(rMode), fpath,
                        'Conary CharacterDevice has RPM non-character-device'
                        ' mode 0%o' %rMode)
                devassert(fpath, rDev, fileObj)

            elif isinstance(fileObj, files.BlockDevice):
                fassert(stat.S_ISBLK(rMode), fpath,
                        'Conary BlockDevice has RPM non-block-device'
                        ' mode 0%o' %rMode)
                devassert(fpath, rDev, fileObj)

            elif isinstance(fileObj, files.NamedPipe):
                fassert(stat.S_ISFIFO(rMode), fpath,
                        'Conary NamedPipe has RPM non-named-pipe'
                        ' mode 0%o' %rMode)
                fassert(not fileObj.flags.isPayload(), fpath,
                        'NamedPipe file is marked as payload')

            elif isinstance(fileObj, files.SymbolicLink):
                fassert(stat.S_ISLNK(rMode), fpath,
                        'Conary SymbolicLink has RPM non-symlink'
                        ' mode 0%o' %rMode)
                fassert(fileObj.target() == rLinkto, fpath,
                        'Symlink target mismatch:'
                        ' RPM %s != Conary %s'
                        %(rLinkto, fileObj.target()))
                fassert(not fileObj.flags.isPayload(), fpath,
                        'SymbolicLink file is marked as payload')

            else:
                # unhandled file type
                fassert(False, fpath,
                        'Unknown Conary file type %r' %fileObj)

            # Now, some tests based on the contents of the RPM header
            if not stat.S_ISDIR(rMode) and rFlags & rpmhelper.RPMFILE_GHOST:
                fassert(fileObj.flags.isInitialContents(), fpath,
                        'RPM ghost non-directory is not InitialContents')

            if rFlags & rpmhelper.RPMFILE_MISSINGOK:
                fassert(fileObj.flags.isMissingOkay(), fpath,
                        'RPM missingok file does not have missingOkay flag')
            if fileObj.flags.isMissingOkay():
                fassert(rFlags & rpmhelper.RPMFILE_MISSINGOK, fpath,
                        'missingOkay file does not have RPM missingok flag')

            if not rVflags:
                # %doc -- CNY-3254
                fassert(not fileObj.flags.isInitialContents(), fpath,
                        'RPM %%doc file is InitialContents')

        # Make sure we have explicitly checked every file in the RPM
        uncheckedFiles = [x[0] for x in foundFiles.iteritems() if not x[1] ]
        fassert(not uncheckedFiles, str(uncheckedFiles),
                'Files contained in RPM not contained in Conary changeset')

        if errors:
            raise ChangesetValidationFailedError(jobId=jobId,
                    reason='\n'.join([
                        '%s: %s' %(x, y) for x, y in errors
                    ]))

    def _sanityCheckChangeSet(self, csFile, jobId):
        """
        Sanity check changeset before commit.
        """

        def idCmp(a, b):
            apid, afid = a[0][0], a[0][2]
            bpid, bfid = b[0][0], b[0][2]

            return cmp((apid, afid), (bpid, bfid))

        newCs = changeset.ChangeSetFromFile(csFile)
        log.info('[%s] comparing changeset to rpm capsules' % jobId)

        capsules = []
        for newTroveCs in newCs.iterNewTroveList():
            if newTroveCs.getTroveInfo().capsule.type() == 'rpm':
                if newTroveCs.getOldVersion():
                    name, version, flavor = newTroveCs.getOldNameVersionFlavor()
                    oldCsJob = [ (name, (None, None),
                                        (version, flavor), True) ]

                    oldCs = self._client.repos.createChangeSet(oldCsJob,
                        withFiles=True, withFileContents=False)

                    oldTroveCs = oldCs.getNewTroveVersion(
                        *newTroveCs.getOldNameVersionFlavor())
                    assert (oldTroveCs.getNewNameVersionFlavor() ==
                            newTroveCs.getOldNameVersionFlavor())
                    oldTrove = trove.Trove(oldTroveCs)
                    newTrove = oldTrove.copy()
                    newTrove.applyChangeSet(newTroveCs)

                else:
                    oldCs = None
                    oldTrove = None
                    newTrove = trove.Trove(newTroveCs)

                fileObjs = []
                # get file streams for comparison
                fileList = list(newTrove.iterFileList(capsules=False))
                for pathId, path, fileId, fileVer in fileList:
                    fileObjs.append(ChangesetFilter._getFileObject(
                        pathId, fileId, oldTrove, oldCs, newCs))

                capFileList = [ x for x in
                    newTrove.iterFileList(capsules=True) ]

                if len(capFileList) != 1:
                    raise FailedToRetrieveChangesetError(jobId=jobId, why='More'
                        ' than 1 RPM capsule in trove %s' % newTroveCs.name())

                capsules.append((capFileList[0], fileList, fileObjs))

        contentsCache = {}
        for capFile, fileList, fileObjs in sorted(capsules, cmp=idCmp):
            if capFile[2] in contentsCache:
                capsuleFileContents = contentsCache[capFile[2]]
            else:
                try:
                    getFileContents = newCs.getFileContents
                    fcList = getFileContents(capFile[0], capFile[2],
                                             compressed=False)
                    capsuleFileContents = fcList[1].get()
                except KeyError, e:
                    getFileContents = self._client.repos.getFileContents
                    fcList = getFileContents((capFile[2:], ), compressed=False)
                    capsuleFileContents = fcList[0].get()
                contentsCache[capFile[2]] = capsuleFileContents

            # do the check
            self._sanityCheckRPMCapsule(jobId, fileList, fileObjs,
                                        capsuleFileContents)

        # Make sure the changeset gets closed so that we don't run out of
        # file descriptors.
        del newCs