Jenkins integration example

Runperf allows to integrate with Jenkins via xunit results and also can deliver html results with great level of details especially for regression testing. Let me present one example which is not meant to be copy&pasted to your environment, but can serve as an inspiration for integration. An overview might look like this:

_images/diagram.png

Which might generate following html results (slightly outdated version that compares a different setting on host and guest): here

Let’s imagine we have example.org machine, we can create rp-example job to run regression testing, then rp-example-manual job to allow testing of changes or custom params. For each of these we might want to create $name-ident jobs to allow cherry-picking and analyzing of results in order to create models to easier evaluate the expected results.

A useful addition is the rp-analysis job to allow running custom compare queries without the need to download and run them from your machines and the rp-prune-artifacts to automatically remove big tarballs with full results and only keep the json results that are small and suffice for compare-perf usecases.

The latest addition is an upstream qemu bisect pipeline called rp-example-upstream-bisect.

The above pipelines are using Jenkins shared library runperf, which is attached below.

All of these can be easily defined via Jenkins Job Builder:

##############################################################################
# Default configuration
##############################################################################
- defaults:
    name: "global"
    mailto: ""
    wrappers:
        - ansicolor
        - timestamps
        - workspace-cleanup
    build-discarder:
        days-to-keep: 365
        artifact-num-to-keep: 60
    # Default runperf params
    param-distro: ''
    param-guest-distro: ''
    param-tests: "'fio:{{\"targets\": \"/fio\"}}' 'uperf:{{\"protocols\": \"tcp\"}}' 'uperf:{{\"protocols\": \"udp\", \"test-types\": \"rr\"}}'"
    param-profiles: "Localhost DefaultLibvirt TunedLibvirt"
    param-src-build: '1'
    param-cmp-tolerance: 5
    param-cmp-stddev-tolerance: 10
    param-cmp-model-job: ''
    param-cmp-model-build: ''
    param-host-rpm-from-urls: ''
    param-no-reference-builds: 14
    param-fio-nbd-setup: false
    param-upstream-qemu-commit: ''
    param-github-publisher: ''
    param-metadata: ''
    param-host-script: ''
    param-worker-script: ''
    param-job: "{name}-run"
    trigger-on: "H 17 * * *"
    disabled: false


##############################################################################
# Definition for the run-perf execution job
##############################################################################
- job-template:
    name: "{name}-run"
    triggers:
        - timed: "{trigger-on}"
    project-type: pipeline
    parameters:
        - string:
            name: DISTRO
            description: 'Distribution to be installed/is installed (Fedora-31), when empty latest el8 nightly build is obtained from bkr'
            default: "{param-distro}"
        - string:
            name: GUEST_DISTRO
            description: 'Distribution to be installed on guest, when empty "distro" is used'
            default: "{param-guest-distro}"
        - string:
            name: MACHINE
            description: 'Machine to be provisioned and tested'
            default: "{param-machine}"
        - string:
            name: ARCH
            description: 'Target machine architecture'
            default: "{param-arch}"
        - string:
            name: TESTS
            description: 'Space separated list of tests to be executed (WARNING: fio-nbd test requires the FIO_NBD_SETUP checkbox enabled!)'
            default: "{param-tests}"
        - string:
            name: PROFILES
            description: 'Space separated list of profiles to be applied'
            default: "{param-profiles}"
        - string:
            name: SRC_BUILD
            description: 'Base build to compare with'
            default: "{param-src-build}"
        - string:
            name: CMP_MODEL_JOB
            description: 'Job to copy linear "model.json" from'
            default: "{param-cmp-model-job}"
        - string:
            name: CMP_MODEL_BUILD
            description: 'Build to copy linear "model.json" from (-1 means lastSuccessful)'
            default: "{param-cmp-model-build}"
        - string:
            name: CMP_TOLERANCE
            description: Tolerance for mean values
            default: "{param-cmp-tolerance}"
        - string:
            name: CMP_STDDEV_TOLERANCE
            description: Tolerance for standard deviation values
            default: "{param-cmp-stddev-tolerance}"
        - string:
            name: HOST_KERNEL_ARGS
            description: Add custom kernel arguments on host
            default: ""
        - text:
            name: HOST_RPM_FROM_URLS
            description: 'Specify pages to query for links to RPMs to be installed on host. Works well with koji/brew links to package or build page as well as individually published list of pkgs, also tries to find "$arch/" link and search pkgs there.\n\nThe format is:\n$pkgFilter;$rpmFilter;$urlList\nkernel-[^"]*fc38;!debug|bpftool|kernel-tools|perf|kernel-selftests|kernel-doc;https://koji.fedoraproject.org/koji//packageinfo?packageID=8\nhttps://koji.fedoraproject.org/koji//buildinfo?buildID=2110910\n(\d+);;http://example.com/repos/test/MyRepo/?C=M\\;O=D'
            default: "{param-host-rpm-from-urls}"
        - string:
            name: GUEST_KERNEL_ARGS
            description: Add custom kernel argsuments on workers/guests
            default: ""
        - text:
            name: GUEST_RPM_FROM_URLS
            description: 'Specify pages to query for links to RPMs to be installed on guest. Works well with koji/brew links to package or build page as well as individually published list of pkgs, also tries to find "$arch/" link and search pkgs there.\n\nThe format is:\n$pkgFilter;$rpmFilter;$urlList\nkernel-[^"]*fc38;!debug|bpftool|kernel-tools|perf|kernel-selftests|kernel-doc;https://koji.fedoraproject.org/koji//packageinfo?packageID=8\nhttps://koji.fedoraproject.org/koji//buildinfo?buildID=2110910\n(\d+);;http://example.com/repos/test/MyRepo/?C=M\\;O=D'
            default: ""
        - bool:
            name: PBENCH_PUBLISH
            description: 'Push the pbench results to company pbench server'
            default: "{param-pbench-publish}"
        - string:
            name: GITHUB_PUBLISHER_PROJECT
            description: 'Github publisher project ID (when you want to publish your results)'
            default: "{param-github-publisher}"
        - bool:
            name: FIO_NBD_SETUP
            description: 'Compile and install fio with nbd ioengine enabled before test execution'
            default: "{param-fio-nbd-setup}"
        - string:
            name: UPSTREAM_QEMU_COMMIT
            description: 'Compile and install qemu using provided commit/tag from the upstream git. Use it by using $PROFILE:{{"qemu_bin": "/usr/local/bin/qemu-system-$ARCH"}} when specifying profiles.'
            default: "{param-upstream-qemu-commit}"
        - bool:
            name: FEDORA_LATEST_KERNEL
            description: 'Install the latest kernel from koji (Fedora rpm)'
            default: false
        - string:
            name: METADATA
            description: 'Additional run-perf --metadata arguments'
            default: "{param-metadata}"
        - text:
            name: HOST_SCRIPT
            description: 'Host script to be executed on all --servers'
            default: "{param-host-script}"
        - text:
            name: WORKER_SCRIPT
            description: 'Worker script to be executed on all runperf workers'
            default: "{param-worker-script}"
        - string:
            name: DESCRIPTION_PREFIX
            description: Description prefix (describe the difference from default)
            default: ""
        - string:
            name: NO_REFERENCE_BUILDS
            description: "Number of reference builds for comparison"
            default: "{param-no-reference-builds}"
    sandbox: true
    pipeline-scm:
        scm:
            - git:
                url: git://PATH_TO_YOUR_REPO_WITH_PIPELINES.git
                branches:
                    - main
        script-path: "runperf.groovy"
        lightweight-checkout: true


##############################################################################
# Definition for the upstream qemu bisect job
##############################################################################
- job-template:
    name: "{name}-bisect-qemu"
    project-type: pipeline
    parameters:
        - string:
            name: DISTRO
            description: 'Distribution to be installed/is installed (Fedora-31), when empty latest el8 nightly build is obtained from bkr'
            default: "{param-distro}"
        - string:
            name: GUEST_DISTRO
            description: 'Distribution to be installed on guest, when empty "distro" is used'
            default: "{param-guest-distro}"
        - string:
            name: MACHINE
            description: 'Machine to be provisioned and tested'
            default: "{param-machine}"
        - string:
            name: ARCH
            description: 'Target machine architecture'
            default: "{param-arch}"
        - string:
            name: TESTS
            description: 'Space separated list of tests to be executed (WARNING: fio-nbd test requires the FIO_NBD_SETUP checkbox enabled!)'
            default: "{param-tests}"
        - string:
            name: PROFILES
            description: 'Space separated list of profiles to be applied'
            default: "{param-profiles}"
        - text:
            name: HOST_SCRIPT
            description: 'Host script to be executed on all --servers'
            default: "{param-host-script}"
        - text:
            name: WORKER_SCRIPT
            description: 'Worker script to be executed on all runperf workers'
            default: "{param-worker-script}"
        - string:
            name: HOST_KERNEL_ARGS
            description: Add custom kernel arguments on host
            default: ""
        - text:
            name: HOST_RPM_FROM_URLS
            description: 'Specify pages to query for links to RPMs to be installed on host. Works well with koji/brew links to package or build page as well as individually published list of pkgs, also tries to find "$arch/" link and search pkgs there.\n\nThe format is:\n$pkgFilter;$rpmFilter;$urlList\nkernel-[^"]*fc38;!debug|bpftool|kernel-tools|perf|kernel-selftests|kernel-doc;https://koji.fedoraproject.org/koji//packageinfo?packageID=8\nhttps://koji.fedoraproject.org/koji//buildinfo?buildID=2110910\n(\d+);;http://example.com/repos/test/MyRepo/?C=M\\;O=D'
            default: "{param-host-rpm-from-urls}"
        - string:
            name: GUEST_KERNEL_ARGS
            description: Add custom kernel argsuments on workers/guests
            default: ""
        - text:
            name: GUEST_RPM_FROM_URLS
            description: 'Specify pages to query for links to RPMs to be installed on guest. Works well with koji/brew links to package or build page as well as individually published list of pkgs, also tries to find "$arch/" link and search pkgs there.\n\nThe format is:\n$pkgFilter;$rpmFilter;$urlList\nkernel-[^"]*fc38;!debug|bpftool|kernel-tools|perf|kernel-selftests|kernel-doc;https://koji.fedoraproject.org/koji//packageinfo?packageID=8\nhttps://koji.fedoraproject.org/koji//buildinfo?buildID=2110910\n(\d+);;http://example.com/repos/test/MyRepo/?C=M\\;O=D'
            default: ""
        - bool:
            name: PBENCH_PUBLISH
            description: 'Push the pbench results to company pbench server'
            default: "{param-pbench-publish}"
        - bool:
            name: FIO_NBD_SETUP
            description: 'Compile and install fio with nbd ioengine enabled before test execution'
            default: "{param-fio-nbd-setup}"
        - bool:
            name: TWO_OUT_OF_THREE
            description: 'Use 2 out of 3 result evaluation (longer duration, better stability for jittery results)'
            default: true
        - string:
            name: UPSTREAM_QEMU_GOOD
            description: 'SHA of the last good (older) upstream qemu.'
            default: "{param-upstream-qemu-commit}"
        - string:
            name: UPSTREAM_QEMU_BAD
            description: 'SHA of the last bad (newer) upstream qemu.'
            default: "{param-upstream-qemu-commit}"
        - string:
            name: DESCRIPTION_PREFIX
            description: Description prefix (describe the difference from default)
            default: ""
    sandbox: true
    pipeline-scm:
        scm:
            - git:
                url: git://PATH_TO_YOUR_REPO_WITH_PIPELINES.git
                branches:
                    - main
        script-path: "upstream_bisect.groovy"
        lightweight-checkout: true


##############################################################################
# Definition for a multi-run-perf execution job
##############################################################################
- job-template:
    name: "{name}-multi"
    project-type: pipeline
    parameters:
        - string:
            name: JOB_NAME
            description: 'Name of the run-perf job to be used for triggering the tests.\nWarning: there might be concurrency issues in case one attempts to concurrently schedule jobs!'
            default: "{param-job}"
        - string:
            name: MACHINE
            description: 'Machine to be provisioned and tested'
            default: "{param-machine}"
        - string:
            name: ARCH
            description: 'Target machine architecture'
            default: "{param-arch}"
        - string:
            name: TESTS
            description: 'Space separated list of tests to be executed (WARNING: fio-nbd test requires the FIO_NBD_SETUP checkbox enabled!)'
            default: "{param-tests}"
        - string:
            name: PROFILES
            description: 'Space separated list of profiles to be applied'
            default: "{param-profiles}"
        - string:
            name: CMP_MODEL_JOB
            description: 'Job to copy linear "model.json" from'
            default: "{param-cmp-model-job}"
        - string:
            name: CMP_MODEL_BUILD
            description: 'Build to copy linear "model.json" from (-1 means lastSuccessful)'
            default: "{param-cmp-model-build}"
        - string:
            name: CMP_TOLERANCE
            description: Tolerance for mean values
            default: "{param-cmp-tolerance}"
        - string:
            name: CMP_STDDEV_TOLERANCE
            description: Tolerance for standard deviation values
            default: "{param-cmp-stddev-tolerance}"
        - bool:
            name: FIO_NBD_SETUP
            description: 'Compile and install fio with nbd ioengine enabled before test execution'
            default: "{param-fio-nbd-setup}"
        - string:
            name: DESCRIPTION_PREFIX
            description: Description prefix (describe the difference from default)
            default: ""
        - bool:
            name: PBENCH_PUBLISH
            description: 'Push the pbench results to company pbench server'
            default: "{param-pbench-publish}"
        - string:
            name: GITHUB_PUBLISHER_PROJECT
            description: 'Github publisher project ID (when you want to publish your results)'
            default: ""
        - text:
            name: HOST_SCRIPT
            description: 'Host script to be executed on all --servers'
            default: "{param-host-script}"
        - text:
            name: WORKER_SCRIPT
            description: 'Worker script to be executed on all runperf workers'
            default: "{param-worker-script}"

        - string:
            name: NO_ITERATIONS
            description: 'How many times to run each iteration'
            default: ''
        - string:
            name: DISTROS
            description: '`;` separated list of distributions to be installed/is installed (Fedora-31), when empty latest el8 nightly build is obtained from bkr, when `..` is used it uses bkr to fill all available versions in between the specified versions'
            default: "{param-distro}"
        - string:
            name: GUEST_DISTROS
            description: '`;` separated list of distribution to be installed on guest, when empty "distro" is used, when `..` is used it uses bkr to fill all available versions in between the specified versions'
            default: "{param-guest-distro}"
        - string:
            name: HOST_KERNEL_ARGSS
            description: '`;` separated list of Add custom kernel arguments on host'
            default: ""
        - text:
            name: HOST_RPM_FROM_URLSS
            description: 'Double enter (\\n\\n) separated list of Single enter (\\n) separated list of pages to query for links to RPMs to be installed on host. Works well with koji/brew links to package or build page as well as individually published list of pkgs, also tries to find "$arch/" link and search pkgs there.\n\nThe format is:\n$pkgFilter;$rpmFilter;$urlList\nkernel-[^"]*fc38;!debug|bpftool|kernel-tools|perf|kernel-selftests|kernel-doc;https://koji.fedoraproject.org/koji//packageinfo?packageID=8\nhttps://koji.fedoraproject.org/koji//buildinfo?buildID=2110910\n(\d+);;http://example.com/repos/test/MyRepo/?C=M\\;O=D'
            default: "{param-host-rpm-from-urls}"
        - string:
            name: GUEST_KERNEL_ARGSS
            description: '`;` separated list of custom kernel argsuments on workers/guests'
            default: ""
        - text:
            name: GUEST_RPM_FROM_URLSS
            description: 'Double enter (\\n\\n) separated list of Single enter (\\n) separated list of pages to query for links to RPMs to be installed on guest. Works well with koji/brew links to package or build page as well as individually published list of pkgs, also tries to find "$arch/" link and search pkgs there.\n\nThe format is:\n$pkgFilter;$rpmFilter;$urlList\nkernel-[^"]*fc38;!debug|bpftool|kernel-tools|perf|kernel-selftests|kernel-doc;https://koji.fedoraproject.org/koji//packageinfo?packageID=8\nhttps://koji.fedoraproject.org/koji//buildinfo?buildID=2110910\n(\d+);;http://example.com/repos/test/MyRepo/?C=M\\;O=D'
            default: ""
        - string:
            name: UPSTREAM_QEMU_COMMITS
            description: '`;` separated list of qemu commit/tags to be deployed from the upstream git. Use it by using $PROFILE:{{"qemu_bin": "/usr/local/bin/qemu-system-$ARCH"}} when specifying profiles.'
            default: "{param-upstream-qemu-commit}"
    sandbox: true
    pipeline-scm:
        scm:
            - git:
                url: git://PATH_TO_YOUR_REPO_WITH_PIPELINES.git
                branches:
                    - main
        script-path: "multi_runperf.groovy"
        lightweight-checkout: true


##############################################################################
# Definition of the analyze-perf job
##############################################################################
- job-template:
    name: "rp-analysis-{user}"
    project-type: pipeline
    concurrent: false
    description: |
        This job allows to cherry-pick results from runperf job and redo the analysis. It is
        not thread-safe, therefor it is advised to copy this job with user-suffix and run
        the analysis in series storing the graphs manually before submitting next comparison.
    parameters:
        - string:
            name: SRC_JOB
            default: "{param-src-job}"
            desciption: Source jenkins job
        - string:
            name: BUILDS
            default: ""
            description: "List of space separated build numbers to be analyzed, first build is used as source build (not included in graphs)"
        - string:
            name: DESCRIPTION
            default: ""
            description: Description of this analysis
        - string:
            name: CMP_MODEL_JOB
            description: 'Job to copy linear "model.json" from'
            default: "{param-cmp-model-job}"
        - string:
            name: CMP_MODEL_BUILD
            description: 'Build to copy linear "model.json" from (-1 means lastSuccessful)'
            default: "{param-cmp-model-build}"
        - string:
            name: CMP_TOLERANCE
            description: Tolerance for mean values
            default: "{param-cmp-tolerance}"
        - string:
            name: CMP_STDDEV_TOLERANCE
            description: Tolerance for standard deviation values
            default: "{param-cmp-stddev-tolerance}"
    sandbox: true
    pipeline-scm:
        scm:
            - git:
                url: git://PATH_TO_YOUR_REPO_WITH_PIPELINES.git
                branches:
                    - main
        script-path: "compareperf.groovy"
        lightweight-checkout: true


##############################################################################
# Definition of the analyze-perf job
##############################################################################
- job-template:
    name: "{name}-identify"
    project-type: pipeline
    description: |
        This job uses analyze-perf script to create model that can be used to better
        evaluate run-perf results.
    parameters:
        - string:
            name: SRC_JOB
            default: "{name}-run"
            desciption: Source jenkins job
        - string:
            name: BUILDS
            default: ""
            description: "List of space separated build numbers to be used"
        - string:
            name: DESCRIPTION
            default: ""
            description: Free-form description
        - string:
            name: EXTRA_ARGS
            default: ""
            description: Additional analyze-perf arguments, for example -t to override default tolerance
        - string:
            name: REBASE_MODEL_BUILD
            description: 'Build number of this job to be used as "--rebase-model" argument'
            default: ""
    sandbox: true
    pipeline-scm:
        scm:
            - git:
                url: git://PATH_TO_YOUR_REPO_WITH_PIPELINES.git
                branches:
                    - main
        script-path: "identify.groovy"
        lightweight-checkout: true


##############################################################################
# Definition of the prune artifacts job
##############################################################################
- job-template:
    name: "rp-prune-artifacts"
    node: master
    description: |
        Remove the big tar.xz files from oldish results not tagged
        as keep-forewer.
    triggers:
        - timed: "H 06 * * *"
    parameters:
        - string:
            name: JOB
            default: "{list,your,runperf,jobs,here,to,clean,them,daily}"
            description: Name of the job to be pruned
        - string:
            name: AGE
            default: "{param-age}"
            description: How old results should be pruned
    builders:
        - python:
            !include-raw-escape: ../scripts/prune_artifacts.py


##############################################################################
# Definition of the git-publisher job
##############################################################################
- job-template:
    name: "rp-publish-results-git"
    description: |
        Publish the build result in git so it can be viewed eg. in github
        pages
    project-type: pipeline
    parameters:
        - string:
            name: JOB
            default: ""
            description: "Job containing the result"
        - string:
            name: BUILD
            default: ""
            description: "Build of the job with the result"
        - bool:
            name: STATUS
            default: false
            description: "Status of the comparison (GOOD=true/BAD=false)"
        - string:
            name: NOTES
            default: ""
            description: "Notes to be added as description to the result entry"
        - string:
            name: PROJECT
            default: ""
            description: "Owner of the results (usually a group/company name + project/machine)"
        - string:
            name: TAG
            default: "all"
            description: "Version tag used to split results of different versions/tags"
        - bool:
            name: STRIP_RESULTS
            default: true
            description: "Publish stripped results (MB->KB)"
        - string:
            name: OS_VERSION
            default: ""
            description: "Override the os version"
        - string:
            name: QEMU_SHA
            defalut: ""
            description: "Override the qemu SHA"
    pipeline-scm:
        scm:
            - git:
                url: git://PATH_TO_YOUR_REPO_WITH_PIPELINES.git
                branches:
                    - main
        script-path: "publish-results-git.groovy"
        lightweight-checkout: true

###############################################################################
## Project to define jobs for automated regression jobs on example.org machine
###############################################################################
#- project:
#    name: rp-example
#    param-machine: "example.org"
#    param-arch: "x86_64"
#    param-src-build: 1
#    param-cmp-model-job: "{name}-identify"
#    param-cmp-model-build: -1
#    param-pbench-publish: true
#    jobs:
#        - "{name}-run"
#        - "{name}-identify"
#
#
###############################################################################
## Project to define manual jobs for example.org machine
###############################################################################
#- project:
#    name: rp-example-manual
#    param-machine: "example.org"
#    param-arch: "x86_64"
#    param-distro: "YOUR STABLE RELEASE"
#    param-src-build: 1
#    param-cmp-model-job: "rp-example-manual-identify"
#    param-cmp-model-build: 1
#    param-pbench-publish: false
#    trigger-on: ""
#    jobs:
#        - "{name}-run"
#        - "{name}-identify"
#        - "{name}-multi":
#            param-cmp-model-job: ''
#            param-cmp-model-build: ''
#
#
###############################################################################
## Project to allow users to run custom queries out of existing results
###############################################################################
#- project:
#    name: rp-analysis
#    user:
#        - virt
#    param-src-job: "rp-example-manual"
#    param-cmp-model-job: "rp-example-manual-identify"
#    param-cmp-model-build: 1
#    jobs:
#        - "rp-analysis-{user}"
#
###############################################################################
## Prune artifacts after 14 days, hopefully we would notice and mark/move
## them when full details are needed.
###############################################################################
#- project:
#    name: rp-prune-artifacts
#    param-age: 14
#    jobs:
#        - "rp-prune-artifacts"

Now let’s have a look at the runperf.groovy pipeline:

// Pipeline to run runperf and compare to given results
// groovylint-disable-next-line
@Library('runperf') _

// Following `params` have to be defined in job (eg. via jenkins-job-builder)

// Machine to be provisioned and tested
machine = params.MACHINE.trim()
// target machine's architecture
arch = params.ARCH.trim()
// Distribution to be installed/is installed (Fedora-32)
// when empty it will pick the latest available nightly el8
_distro = params.DISTRO.trim()
_distro = _distro ?: 'latest-RHEL-8.0%.n.%'
// Distribution to be installed on guest, when empty "distro" is used
guestDistro = params.GUEST_DISTRO.trim()
// Space separated list of tests to be executed
tests = params.TESTS.trim()
// Space separated list of profiles to be applied
profiles = params.PROFILES.trim()
// Base build to compare with
srcBuild = params.SRC_BUILD.trim()
// Compareperf tollerances
cmpModelJob = params.CMP_MODEL_JOB.trim()
cmpModelBuild = params.CMP_MODEL_BUILD.trim()
cmpTolerance = params.CMP_TOLERANCE.trim()
cmpStddevTolerance = params.CMP_STDDEV_TOLERANCE.trim()
// Add custom kernel arguments on host
hostKernelArgs = params.HOST_KERNEL_ARGS.trim()
// Install rpms from (beaker) urls
hostRpmFromURLs = params.HOST_RPM_FROM_URLS.trim()
// Add custom kernel argsuments on workers/guests
guestKernelArgs = params.GUEST_KERNEL_ARGS.trim()
// Install rpms from (beaker) urls
guestRpmFromURLs = params.GUEST_RPM_FROM_URLS.trim()
// Add steps to fetch, compile and install the upstream fio with nbd ioengine compiled in
fioNbdSetup = params.FIO_NBD_SETUP
// Add steps to checkout, compile and install the upstream qemu from git
upstreamQemuCommit = params.UPSTREAM_QEMU_COMMIT.trim()
// Add steps to install the latest kernel from koji (Fedora rpm)
fedoraLatestKernel = params.FEDORA_LATEST_KERNEL
// Description prefix (describe the difference from default)
descriptionPrefix = params.DESCRIPTION_PREFIX
// Number of reference builds
noReferenceBuilds = params.NO_REFERENCE_BUILDS.toInteger()
// Pbench-publish related options
pbenchPublish = params.PBENCH_PUBLISH
// Github-publisher project ID
githubPublisherProject = params.GITHUB_PUBLISHER_PROJECT.trim()
githubPublisherTag = ''
// Additional run-perf metadata
metadata = params.METADATA
// Custom host/guest setups cript
hostScript = params.HOST_SCRIPT
workerScript = params.WORKER_SCRIPT

// Extra variables
// Provisioner machine
workerNode = 'runperf-slave'
// runperf git branch
gitBranch = 'main'
// extra runperf arguments
extraArgs = ''

node(workerNode) {
    stage('Preprocess') {
        (distro, guestDistro, descriptionPrefix) = runperf.preprocessDistros(_distro, guestDistro,
                                                                             arch, descriptionPrefix)
        currentBuild.description = "${distro} - in progress"
    }

    stage('Measure') {
        runperf.deployDownstreamConfig(gitBranch)
        runperf.deployRunperf(gitBranch)
        if (fedoraLatestKernel) {
            kernelURL = '\nkernel-;!debug|kernel-selftests|kernel-doc;https://koji.fedoraproject.org/koji//packageinfo?packageID=8'
            hostRpmFromURLs += kernelURL
            guestRpmFromURLs += kernelURL
        }
        // Use grubby to update default args on host
        hostScript = runperf.setupScript(hostScript, hostKernelArgs, hostRpmFromURLs, arch, fioNbdSetup)
        workerScript = runperf.setupScript(workerScript, guestKernelArgs, guestRpmFromURLs, arch, fioNbdSetup)
        // Build custom qemu
        if (upstreamQemuCommit) {
            // Always translate the user input into the actual commit and also get the description
            sh 'rm -Rf upstream_qemu'
            dir('upstream_qemu') {
                sh 'git clone --filter=tree:0 https://gitlab.com/qemu-project/qemu.git .'
                upstreamQemuVersion = sh(returnStdout: true,
                                         script: "git rev-parse ${upstreamQemuCommit}").trim()
                githubPublisherTag = sh(returnStdout: true,
                                        script: "git describe --tags --always ${upstreamQemuCommit}"
                                       ).trim().split('-')[0]
                println("Using qemu $githubPublisherTag commit $upstreamQemuVersion")
            }
            sh '\\rm -Rf upstream_qemu'
            hostScript += '\n\n' + String.format(runperf.upstreamQemuScript, upstreamQemuVersion, upstreamQemuVersion)
        }
        if (hostScript) {
            writeFile file: 'host_script', text: hostScript
            extraArgs += ' --host-setup-script host_script --host-setup-script-reboot'
        }
        if (workerScript) {
            writeFile file: 'worker_script', text: workerScript
            extraArgs += ' --worker-setup-script worker_script'
        }
        if (pbenchPublish) {
            metadata += ' pbench_server_publish=yes'
        }
        // Using jenkins locking to prevent multiple access to a single machine
        lock(machine) {
            sh '$KINIT'
            status = sh(returnStatus: true,
               script: "python3 scripts/run-perf ${extraArgs} -v --hosts ${machine} --distro ${distro} " +
               "--provisioner Beaker --default-password YOUR_DEFAULT_PASSWORD --profiles ${profiles} " +
               '--log run.log --paths ./downstream_config --metadata ' +
               "'build=${currentBuild.number}${descriptionPrefix}' " +
               "'url=${currentBuild.absoluteUrl}' 'project=YOUR_PROJECT_ID ${currentBuild.projectName}' " +
               "'pbench_server=YOUR_PBENCH_SERVER_URL' " +
               "'machine_url_base=https://YOUR_BEAKER_URL/view/%(machine)s' " +
               "${metadata} -- ${tests}")
        }
        // Add new-line after runperf output (ignore error when does not exists
        sh(returnStatus: true, script: "echo >> \$(echo -n result*)/RUNPERF_METADATA")
        stage('Archive results') {
            // Archive only "result_*" as we don't want to archive "resultsNoArchive"
            sh returnStatus: true, script: 'tar cf - result_* | xz -T2 -7e - > "$(echo result_*)".tar.xz'
            archiveArtifacts allowEmptyArchive: true, artifacts: runperf.runperfArchiveFilter
        }
        if (status) {
            runperf.tryOtherDistros(_distro, arch)
            runperf.failBuild('Run-perf execution failed',
                              "run-perf returned non-zero status ($status)",
                              distro)
        }
    }

    stage('Compare') {
        // Get up to noReferenceBuilds json results to use as a reference
        referenceBuilds = []
        for (build in runperf.getGoodBuildNumbers(env.JOB_NAME)) {
            copyArtifacts(filter: runperf.runperfResultsFilter, optional: true,
                          fingerprintArtifacts: true, projectName: env.JOB_NAME, selector: specific("${build}"),
                          target: "reference_builds/${build}/")
            if (findFiles(glob: "reference_builds/${build}/result*/*/*/*/*.json")) {
                referenceBuilds.add("${build}:" + sh(returnStdout: true,
                                     script: "echo reference_builds/${build}/*").trim())
                if (referenceBuilds.size() >= noReferenceBuilds) {
                    break
                }
            }
        }
        // Get src build's json results to compare against
        copyArtifacts(filter: runperf.runperfResultsFilter, optional: true,
                      fingerprintArtifacts: true, projectName: env.JOB_NAME, selector: specific(srcBuild),
                      target: 'src_result/')
        // If model build set get the model from it's job
        if (cmpModelBuild) {
            if (cmpModelBuild == '-1') {
                copyArtifacts(filter: runperf.modelJson, optional: false, fingerprintArtifacts: true,
                              projectName: cmpModelJob, selector: lastSuccessful(), target: runperf.thisPath)
            } else {
                copyArtifacts(filter: runperf.modelJson, optional: false, fingerprintArtifacts: true,
                              projectName: cmpModelJob, selector: specific(cmpModelBuild), target: runperf.thisPath)
            }
            cmpExtra = '--model-linear-regression ' + runperf.modelJson
        } else {
            cmpExtra = ''
        }
        // Compare the results and generate html as well as xunit results
        status = sh(returnStatus: true,
                    script: ('python3 scripts/compare-perf --log compare.log ' +
                             '--tolerance ' + cmpTolerance + ' --stddev-tolerance ' + cmpStddevTolerance +
                             " --xunit ${runperf.resultXml} --html ${runperf.htmlIndex} --html-small-file " + cmpExtra +
                             ' -- src_result/* ' + referenceBuilds.reverse().join(' ') +
                             ' $(find . -maxdepth 1 -type d ! -name "*.tar.*" -name "result*")'))
        if (fileExists(runperf.resultXml)) {
            if (status) {
                // This could mean there were no tests to compare or other failures, interrupt the build
                echo "Non-zero exit status: ${status}"
            }
        } else {
            runperf.failBuild('Compare-perf execution failed',
                              "Missing ${runperf.resultXml}, exit code: ${status}",
                              distro)
        }
    }

    stage('Postprocess') {
        // Build description
        currentBuild.description = "${descriptionPrefix}${srcBuild} ${currentBuild.number} ${distro}"
        // Store and publish html results
        archiveArtifacts allowEmptyArchive: true, artifacts: runperf.htmlIndex
        if (fileExists(runperf.htmlPath)) {
            publishHTML([allowMissing: true, alwaysLinkToLastBuild: false, keepAll: true, reportDir: runperf.htmlPath,
                         reportFiles: runperf.htmlFile, reportName: 'HTML Report', reportTitles: ''])
        }
        // Junit results
        junit allowEmptyResults: true, testResults: runperf.resultXml
        // Remove the unnecessary big files
        sh runperf.runperfArchFilterRmCmd
        // Publish the results
        if (githubPublisherProject) {
            build(job: 'rp-publish-results-git',
                   parameters: [string(name: 'JOB', value: env.JOB_NAME),
                                string(name: 'BUILD', value: env.BUILD_NUMBER),
                                booleanParam(name: 'STATUS', value: status == 0),
                                string(name: 'NOTES', value: descriptionPrefix),
                                string(name: 'PROJECT', value: githubPublisherProject),
                                string(name: 'TAG', value: githubPublisherTag),
                                booleanParam(name: 'STRIP_RESULTS', value: true)],
                   quietPeriod: 0,
                   wait: false)
        }
    }
}

Following compareperf.groovy pipeline is extremely useful for later analysis, or extra comparison of manual pipelines:

// Pipeline to create comparison of previously generated runperf results
// Following `params` have to be defined in job (eg. via jenkins-job-builder)
// groovylint-disable-next-line
@Library('runperf') _

// Source jenkins job
srcJob = params.SRC_JOB.trim()
// List of space separated build numbers to be analyzed, first build is used
// as source build (not included in graphs)
builds = params.BUILDS.split().toList()
// Description of this analysis
description = params.DESCRIPTION
// Compareperf tollerances
cmpModelJob = params.CMP_MODEL_JOB.trim()
cmpModelBuild = params.CMP_MODEL_BUILD.trim()
cmpTolerance = params.CMP_TOLERANCE.trim()
cmpStddevTolerance = params.CMP_STDDEV_TOLERANCE.trim()

// Extra variables
// Provisioner machine
workerNode = 'runperf-slave'
// runperf git branch
gitBranch = 'main'
// misc variables
thisPath = '.'
spaceChr = ' '
lastBuildChr = '-1'

stage('Analyze') {
    node(workerNode) {
        assert builds.size() >= 2
        runperf.deployRunperf(gitBranch)
        referenceBuilds = []
        // Get all the reference builds (second to second-to-last ones)
        if (builds.size() > 2) {
            for (build in builds[1..-2]) {
                copyArtifacts(filter: runperf.runperfResultsFilter, optional: true,
                              fingerprintArtifacts: true, projectName: srcJob, selector: specific(build),
                              target: "reference_builds/${build}/")
                if (fileExists("reference_builds/${build}")) {
                    referenceBuilds.add("${build}:" + sh(returnStdout: true,
                                                          script: "echo reference_builds/${build}/*").trim())
                } else {
                    echo "Skipping reference build ${build}, failed to copy artifacts."
                }
            }
        }
        // Get the source build
        copyArtifacts(filter: runperf.runperfResultsFilter, optional: false,
                      fingerprintArtifacts: true, projectName: srcJob, selector: specific(builds[0]),
                      target: 'src_result/')
        // Get the destination build
        copyArtifacts(filter: runperf.runperfResultsFilter, optional: false,
                      fingerprintArtifacts: true, projectName: srcJob, selector: specific(builds[-1]),
                      target: thisPath)
        // Get the model
        if (cmpModelBuild) {
            if (cmpModelBuild == lastBuildChr) {
                copyArtifacts(filter: runperf.modelJson, optional: false, fingerprintArtifacts: true,
                              projectName: cmpModelJob, selector: lastSuccessful(), target: thisPath)
            } else {
                copyArtifacts(filter: runperf.modelJson, optional: false, fingerprintArtifacts: true,
                              projectName: cmpModelJob, selector: specific(cmpModelBuild), target: thisPath)
            }
            cmpExtra = '--model-linear-regression ' + runperf.modelJson
        } else {
            cmpExtra = ''
        }
        status = 0
        lock(workerNode) {
            // Avoid modifying workerNode's environment while executing compareperf
            sh runperf.pythonDeployCmd
            status = sh(returnStatus: true,
                        script: ('python3 scripts/compare-perf -vvv --tolerance ' + cmpTolerance +
                                 ' --stddev-tolerance ' + cmpStddevTolerance +
                                 ' --xunit ' + runperf.resultXml + ' --html ' + runperf.htmlIndex + spaceChr +
                                 cmpExtra + ' -- src_result/* ' + referenceBuilds.join(spaceChr) +
                                 ' $(find . -maxdepth 1 -type d ! -name "*.tar.*" -name "result*")'))
        }
        if (fileExists(runperf.resultXml)) {
            if (status) {
                // This could mean there were no tests to compare or other failures, interrupt the build
                echo "Non-zero exit status: ${status}"
            }
        } else {
            currentBuild.result = 'FAILED'
            error "Missing ${runperf.resultXml}, exit code: ${status}"
        }
        currentBuild.description = "${description}${builds} ${srcJob}"
        archiveArtifacts allowEmptyArchive: true, artifacts: runperf.htmlIndex
        junit allowEmptyResults: true, testResults: runperf.resultXml
        if (fileExists(runperf.htmlPath)) {
            publishHTML([allowMissing: true, alwaysLinkToLastBuild: false, keepAll: true, reportDir: runperf.htmlPath,
                         reportFiles: runperf.htmlFile, reportName: 'HTML Report', reportTitles: ''])
        }
        // Remove the unnecessary big files
        sh '\\rm -Rf result* src_result* reference_builds'
    }
}

And the identify.groovy to allow creating linear models:

// Pipeline to create comparison of previously generated runperf results
// groovylint-disable-next-line
@Library('runperf') _

// Following `params` have to be defined in job (eg. via jenkins-job-builder)
// Source jenkins job
srcJob = params.SRC_JOB
// List of space separated build numbers to be analyzed, first build is used
// as source build (not included in graphs)
builds = params.BUILDS.split().toList()
// Description of this analysis
description = params.DESCRIPTION
// Extra AnalyzePerf arguments
extraArgs = params.EXTRA_ARGS
// Build number used for --rebase-model
rebaseModelBuild = params.REBASE_MODEL_BUILD

// Extra variables
// Provisioner machine
workerNode = 'runperf-slave'
// runperf git branch
gitBranch = 'main'
// misc variables
spaceChr = ' '

stage('Analyze') {
    node(workerNode) {
        runperf.deployRunperf(gitBranch)
        // Get all the specified builds
        for (build in builds) {
            copyArtifacts(filter: 'result*/**/result*.json', optional: false, fingerprintArtifacts: true,
                          projectName: srcJob, selector: specific(build), target: 'results/')
        }
        // If rebaseModel set, get the model from that build
        if (rebaseModelBuild) {
            copyArtifacts(filter: runperf.modelJson, optional: false, fingerprintArtifacts: true,
                          projectName: env.JOB_NAME, selector: specific(rebaseModelBuild),
                          target: 'src_model/')
            extraArgs += " --rebase-model 'src_model/$runperf.modelJson'"
        }
        status = 0
        lock(workerNode) {
            // Avoid modifying workerNode's environment while executing compareperf
            sh 'python3 setup.py develop --user'
            status = sh(returnStatus: true,
                        script: ('python3 scripts/analyze-perf -vvv --stddev-linear-regression ' +
                                 runperf.modelJson + spaceChr + extraArgs + ' -- results/*'))
        }
        if (fileExists(runperf.modelJson)) {
            // This could mean there were no tests to compare or other failures, interrupt the build
            if (status) {
                echo "Non-zero exit status: ${status}"
            }
        } else {
            currentBuild.result = 'FAILED'
            error "Missing ${runperf.modelJson}, exit code: ${status}"
        }
        if (description) {
            currentBuild.description = description + spaceChr + builds.join(spaceChr)
        } else {
            currentBuild.description = builds.join(spaceChr)
        }
        archiveArtifacts allowEmptyArchive: true, artifacts: runperf.modelJson
        sh '\\rm -Rf results*'
    }
}

The cleanup job prune_artifacts.py:

#!/bin/env python3
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
#
# See LICENSE for more details.
#
# Copyright: Red Hat Inc. 2020
# Author: Lukas Doktor <ldoktor@redhat.com>
"""
When executed on a jenkins master it allows to walk the results and remove
"*.tar.*" files on older builds that are not manually marked as
keep-for-infinity.
"""

import time
import glob
import os
import re

JENKINS_DIR = "/var/lib/jenkins/jobs/"


def prune_result(path, before):
    """
    Prune result if older than age and keep forever not set
    """
    build_path = os.path.join(path, "build.xml")
    if not os.path.exists(build_path):
        print("KEEP  %s - no build.xml" % path)
        return
    treated_path = os.path.join(path, "ld_artifact_pruned")
    if os.path.exists(treated_path):
        print("SKIP  %s - already treated" % path)
        return
    with open(build_path) as build_fd:
        build_xml = build_fd.read()
    if "<keepLog>false</keepLog>" not in build_xml:
        print("KEEP  %s - keep forever set" % path)
        return
    match = re.findall(r"<startTime>(\d+)</startTime>", build_xml)
    if not match:
        print("KEEP  %s - no startTime\n%s" % (path, build_xml))
        return
    start_time = int(match[-1])
    if start_time > before:
        print("KEEP  %s - younger than %s (%s)" % (path, before, start_time))
        return
    print("PRUNE %s (%s)" % (path, start_time))
    for pth in glob.glob(os.path.join(path, "archive", "*.tar.*")):
        os.unlink(pth)
    with open(treated_path, 'wb'):
        """touching the file"""


def prune_results(job, age):
    """
    Walk job's builds and prune them
    """
    if not job:
        print("No job specified, returning")
        return
    # Jenkins stores startTime * 1000
    before = int((time.time() - age) * 1000)
    print("Pruning %s builds older than %s" % (job, before))
    builds = glob.glob(os.path.join(JENKINS_DIR, job, "builds", "*"))
    for build in builds:
        prune_result(build, before)
    print("Done")


if __name__ == '__main__':
    prune_results(os.environ.get('JOB'),
                  int(os.environ.get('AGE', 14)) * 86400)

And a bisect job upstream_bisect.groovy:

// Pipeline to run runperf and compare to given results
// groovylint-disable-next-line
@Library('runperf') _

// Following `params` have to be defined in job (eg. via jenkins-job-builder)
// Machine to be provisioned and tested
machine = params.MACHINE.trim()
// target machine's architecture
arch = params.ARCH.trim()
// Distribution to be installed/is installed (Fedora-32)
// when empty it will pick the latest available nightly el8
distro = params.DISTRO.trim()
// Distribution to be installed on guest, when empty "distro" is used
guestDistro = params.GUEST_DISTRO.trim()
// Space separated list of tests to be executed
tests = params.TESTS.trim()
// Space separated list of profiles to be applied
profiles = params.PROFILES.trim()
// Add custom kernel arguments on host
hostKernelArgs = params.HOST_KERNEL_ARGS.trim()
// Install rpms from (beaker) urls
hostRpmFromURLs = params.HOST_RPM_FROM_URLS.trim()
// Add custom kernel argsuments on workers/guests
guestKernelArgs = params.GUEST_KERNEL_ARGS.trim()
// Install rpms from (beaker) urls
guestRpmFromURLs = params.GUEST_RPM_FROM_URLS.trim()
// Add steps to fetch, compile and install the upstream fio with nbd ioengine compiled in
fioNbdSetup = params.FIO_NBD_SETUP
// Specify the bisection range
// Older commit
upstreamQemuGood = params.UPSTREAM_QEMU_GOOD.trim()
// Newer commit
upstreamQemuBad = params.UPSTREAM_QEMU_BAD.trim()
// Description prefix (describe the difference from default)
descriptionPrefix = params.DESCRIPTION_PREFIX
// Pbench-publish related options
pbenchPublish = params.PBENCH_PUBLISH
// Custom host/guest setups cript
hostScript = params.HOST_SCRIPT
workerScript = params.WORKER_SCRIPT

// Extra variables
// Provisioner machine
workerNode = 'runperf-slave'
// runperf git branch
gitBranch = 'main'
// extra runperf arguments
extraArgs = ''

String getBkrInstallCmd(String hostBkrLinks, String hostBkrLinksFilter, String arch) {
    return ('\nfor url in ' + hostBkrLinks + '; do dnf install -y --allowerasing ' +
            '$(curl -k \$url | grep -o -e "http[^\\"]*' + arch + '\\.rpm" -e ' +
            '"http[^\\"]*noarch\\.rpm" | grep -v $(for expr in ' + hostBkrLinksFilter + '; do ' +
            'echo -n " -e $expr"; done)); done')
}

node(workerNode) {
    stage('Preprocess') {
        (distro, guestDistro, descriptionPrefix) = runperf.preprocessDistros(distro, guestDistro,
                                                                             arch, descriptionPrefix)
        currentBuild.description = "${distro} - in progress"
    }

    stage('Measure') {
        runperf.deployDownstreamConfig(gitBranch)
        runperf.deployRunperf(gitBranch)
        metadata = ''
        hostScript = runperf.setupScript(hostScript, hostKernelArgs, HostRpmFromURLs, arch, fioNbdSetup)
        workerScript = runperf.setupScript(workerScript, guestKernelArgs, GuestRpmFromURLs, arch, fioNbdSetup)
        writeFile file: 'host_script', text: hostScript
        setupQemu = String.format(runperf.upstreamQemuScript, upstreamQemuGood, upstreamQemuGood)
        writeFile(file: 'host_script_with_qemu',
                  text: hostScript + '\n\n' + setupQemu)
        if (workerScript) {
            writeFile file: 'worker_script', text: workerScript
            extraArgs += ' --worker-setup-script worker_script'
        }
        if (pbenchPublish) {
            metadata += ' pbench_server_publish=yes'
        }
        // Using jenkins locking to prevent multiple access to a single machine
        lock(machine) {
            // Make sure we have the full upstream_qemu cloned (we don't need submodules, thought)
            sh 'rm -Rf upstream_qemu/'
            sh 'git clone https://gitlab.com/qemu-project/qemu.git upstream_qemu/'
            sh '$KINIT'
            // First run the provisioning and dummy test to age the machine a bit
            sh("python3 scripts/run-perf ${extraArgs} -v --hosts ${machine} --distro ${distro} " +
               '--host-setup-script host_script_with_qemu --host-setup-script-reboot ' +
               '--provisioner Beaker --default-password YOUR_DEFAULT_PASSWORD ' +
               '--profiles DefaultLibvirt --paths ./downstream_config --log prejob.log -- ' +
              '\'fio:{"runtime": "30", "targets": "/fio", "block-sizes": "4", "test-types": "read", ' +
              '"samples": "1"}\'')
            // And now run the bisection without reprovisioning
            sh("DIFFPERF='python3 scripts/diff-perf' contrib/upstream_qemu_bisect.sh upstream_qemu/ " +
               "${upstreamQemuGood} ${upstreamQemuBad} python3 scripts/run-perf ${extraArgs} " +
               "-v --hosts ${machine} --distro ${distro} --log job.log " +
               "--default-password YOUR_DEFAULT_PASSWORD --profiles ${profiles} " +
               "--paths ./downstream_config --metadata " +
               "'project=virt-perf-ci ${currentBuild.projectName}' " +
               "'pbench_server=YOUR_PBENCH_SERVER_URL' " +
               "'machine_url_base=https://YOUR_BEAKER_URL/view/%(machine)s' " +
               "${metadata} -- ${tests}")
        }
    }

    stage('Postprocess') {
        // Build description
        currentBuild.description = "${descriptionPrefix} ${currentBuild.number} ${distro}"
        // Move results to mimic usual run-perf results path
        if (fileExists('.diff-perf/report.html')) {
            diffReportPath = 'html/index.html'
            sh('mkdir -p html')
            sh("mv '.diff-perf/report.html' '$diffReportPath'")
            // Store and publish html results
            archiveArtifacts allowEmptyArchive: true, artifacts: diffReportPath
            publishHTML([allowMissing: true, alwaysLinkToLastBuild: false, keepAll: true, reportDir: 'html/',
                         reportFiles: 'index.html', reportName: 'HTML Report', reportTitles: ''])
        }
        // Remove the unnecessary big files
        sh 'contrib/bisect.sh clean'
    }
}

And a range job to trigger multiple runperf jobs:

// Pipeline to trigger a series of run-perf jobs to cover a range of params.
// Following `params` have to be defined in job (eg. via jenkins-job-builder)
// groovylint-disable-next-line
@Library('runperf') _

csvSeparator = ';'
doubleEnter = '\n\n'

// SHARED VALUES FOR ALL JOBS
// Job name to be triggered
jobName = params.JOB_NAME.trim()
// Machine to be provisioned and tested
machine = params.MACHINE.trim()
// target machine's architecture
arch = params.ARCH.trim()
// Space separated list of tests to be executed
tests = params.TESTS.trim()
// Space separated list of profiles to be applied
profiles = params.PROFILES.trim()
// Compareperf tollerances
cmpModelJob = params.CMP_MODEL_JOB.trim()
cmpModelBuild = params.CMP_MODEL_BUILD.trim()
cmpTolerance = params.CMP_TOLERANCE.trim()
cmpStddevTolerance = params.CMP_STDDEV_TOLERANCE.trim()
// Add steps to fetch, compile and install the upstream fio with nbd ioengine compiled in
fioNbdSetup = params.FIO_NBD_SETUP
// Description prefix (describe the difference from default)
descriptionPrefix = params.DESCRIPTION_PREFIX
// Pbench-publish related options
pbenchPublish = params.PBENCH_PUBLISH
// Github-publisher project ID
githubPublisherProject = params.GITHUB_PUBLISHER_PROJECT.trim()

// LIST OF VALUES
// Iterations of each combination
if (params.NO_ITERATIONS) {
    iterations = (1..params.NO_ITERATIONS.toInteger()).toList()
} else {
    iterations = [1]
}
// Distribution to be installed/is installed (Fedora-32)
// when empty it will pick the latest available nightly el8
distrosRaw = params.DISTROS.split(csvSeparator)
// Distribution to be installed on guest, when empty "distro" is used
guestDistrosRaw = params.GUEST_DISTROS.split(csvSeparator)
// Add custom kernel arguments on host
hostKernelArgss = params.HOST_KERNEL_ARGSS.split(csvSeparator)
// Install rpms from (beaker) urls
hostRpmFromURLss = params.HOST_RPM_FROM_URLSS.trim().split(doubleEnter)
// Add custom kernel argsuments on workers/guests
guestKernelArgss = params.GUEST_KERNEL_ARGSS.split(csvSeparator)
// Install rpms from (beaker) urls
guestRpmFromURLss = params.GUEST_RPM_FROM_URLSS.trim().split(doubleEnter)
// Add steps to checkout, compile and install the upstream qemu from git
upstreamQemuCommits = params.UPSTREAM_QEMU_COMMITS.split(csvSeparator)
// Custom host/guest setups cript
hostScript = params.HOST_SCRIPT
workerScript = params.WORKER_SCRIPT

// Extra variables
// Provisioner machine
workerNode = 'runperf-slave'
// misc variables
srcBuildUnset = '-1'

distros = runperf.getDistrosRange(distrosRaw, workerNode, arch)
guestDistros = runperf.getDistrosRange(guestDistrosRaw, workerNode, arch)

referenceBuilds = 0
srcBuild = srcBuildUnset
paramTypes = [iterations, hostRpmFromURLss, guestKernelArgss, guestRpmFromURLss, hostKernelArgss,
               upstreamQemuCommits, guestDistros, distros]
for (params in paramTypes.combinations()) {
    println("Triggering with: $params")
    if (params[0] == 1) {
        prefix = descriptionPrefix
    } else {
        prefix = "${descriptionPrefix}${params[0]}"
    }
    // TODO: Add no-provisioning-version
    // Use a cleanup job to remove host-setup-script things
    srcBuild = runperf.triggerRunperf(env.JOB_NAME, srcBuild == srcBuildUnset, params[7], params[6],
                                      machine, arch, tests, profiles, srcBuild, params[4],
                                      params[3], params[2], params[1],
                                      params[5], prefix, pbenchPublish,
                                      fioNbdSetup, Math.max(0, referenceBuilds).toString(),
                                      cmpModelJob, cmpModelBuild, cmpTolerance, cmpStddevTolerance,
                                      githubPublisherProject, hostScript, workerScript)
    referenceBuilds += 1
}

To enable the Jenkins shared libraries look at Extending with Shared Libraries tutorial. Once you have your repo enabled, place the following file:

import groovy.transform.Field
import java.util.regex.Pattern

// Use this by adding: @Library('runperf') _

// misc variables
@Field String resultXml = 'result.xml'
@Field String htmlPath = 'html'
@Field String htmlFile = 'index.html'
@Field String htmlIndex = "${htmlPath}/${htmlFile}"
@Field String modelJson = 'model.json'
@Field String thisPath = '.'
@Field String runperfArchiveFilter = ('result*/*/*/*/*.json,result*/RUNPERF_METADATA,result*/**/__error*__/**,' +
                                    'result*/**/__sysinfo*__/**,result_*.tar.xz,*.log')
@Field String runperfArchFilterRmCmd = "\\rm -Rf result* src_result* reference_builds ${htmlPath} *.log"
@Field String runperfResultsFilter = 'result*/*/*/*/*.json,result*/RUNPERF_METADATA,result*/**/__error*__/**'
@Field String makeInstallCmd = '\nmake -j $(getconf _NPROCESSORS_ONLN)\nmake install'
@Field String pythonDeployCmd = 'python3 setup.py develop --user'
@Field String kojiUrl = 'https://koji.fedoraproject.org/koji/'
@Field String fioNbdScript = ('\n\n# FIO_NBD_SETUP' +
                              '\ndnf install --skip-broken -y fio gcc zlib-devel libnbd-devel make qemu-img libaio-devel' +
                              '\ncd /tmp' +
                              '\ncurl -L https://github.com/axboe/fio/archive/fio-3.19.tar.gz | tar xz' +
                              '\ncd fio-fio-3.19' +
                              '\n./configure --enable-libnbd\n' +
                              makeInstallCmd +
                              '\nmkdir -p /var/lib/runperf/' +
                              '\necho "fio 3.19" >> /var/lib/runperf/sysinfo')
// Usage: String.format(upstreamQemuScript, upstreamCommit, upstreamCommit)
@Field String upstreamQemuScript = """# UPSTREAM_QEMU_SETUP
OLD_PWD="\$PWD"
dnf install --skip-broken -y python3-devel zlib-devel gtk3-devel glib2-static spice-server-devel usbredir-devel make gcc libseccomp-devel numactl-devel libaio-devel git ninja-build
cd /root
[ -e "qemu" ] || { mkdir qemu; cd qemu; git init; git remote add origin https://gitlab.com/qemu-project/qemu.git; cd ..; }
cd qemu
git fetch --depth=1 origin %s
git checkout -f %s
git submodule update --init
VERSION=\$(git rev-parse HEAD)
git diff --quiet || VERSION+="-dirty"
./configure --target-list="\$(uname -m)"-softmmu --disable-werror --enable-kvm --enable-vhost-net --enable-attr --enable-fdt --enable-vnc --enable-seccomp --enable-usb-redir --disable-opengl --disable-virglrenderer --with-pkgversion="\$VERSION"
$makeInstallCmd
chcon -Rt qemu_exec_t /usr/local/bin/qemu-system-"\$(uname -m)"
chcon -Rt virt_image_t /usr/local/share/qemu/
\\cp -f build/config.status /usr/local/share/qemu/
cd \$OLD_PWD"""

@Field String bkrExtraArgs = ' --labcontroller ENTER_LAB_CONTROLLER_URL '
@Field String ownerEmail = 'ENTER_OPERATOR_EMAIL_ADDR

void failBuild(String subject, String details, String distro=distro) {
    // Set description, send email and raise exception
    currentBuild.description = "BAD ${distro} - $details"
    mail(to: ownerEmail,
         subject: "${env.JOB_NAME}: $subject",
         body: "Job: ${env.BUILD_URL}\n\n$details")
    error details
}

List preprocessDistros(String distro, String guestDistro, String arch, descriptionPrefix) {
    // Parses the distro and guestDistro params into actual [distro, guestDistro]
    if (distro.startsWith('latest-')) {
        if (distro.startsWith('latest-untested-')) {
            distro = getLatestUntestedDistro(distro[16..-1], arch)
            descriptionPrefix += 'U'    // To emphasize we use "untested" distros
            echo "Using latest-untested distro ${distro} from bkr"
        } else {
            distro = getLatestDistros(distro[7..-1], 1, arch)[0]
            echo "Using latest distro ${distro} from bkr"
        }
    } else {
        echo "Using distro ${distro} from params"
    }
    if (!guestDistro) {
        guestDistro == distro
    }
    if (guestDistro == distro) {
        echo "Using the same guest distro ${distro}"
    } else {
        echo "Using different guest distro: ${guestDistro} from host: ${distro}"
    }
    return [distro, guestDistro, descriptionPrefix]
}

void deployRunperf(gitBranch) {
    git branch: gitBranch, url: 'https://github.com/distributed-system-analysis/run-perf.git'
    // Remove files that might have been left behind
    sh runperfArchFilterRmCmd
    sh "mkdir ${htmlPath}"
    sh pythonDeployCmd
}

void deployDownstreamConfig(gitBranch) {
    // This way we add downstream plugins and other configuration
    dir('downstream_config') {
        git branch: gitBranch, url: 'git://PATH_TO_YOUR_REPO_WITH_PIPELINES/runperf_config.git'
        sh pythonDeployCmd
    }
}

@NonCPS
List urlFindRpms(String url, String rpmFilter, String arch) {
    // Searches html pages for links to RPMs based on the filter
    //
    // The filters to find RPMs is ">${rpmFilter}.*($arch|noarch).rpm<"
    // and it searches (urlList: https://example.com/rpms):
    // 1. the provided $urlList page
    //    - eg: https://example.com/rpms
    // 2. all pages linked from $urlList page using "$arch/?" filter
    //    - eg: https://example.com/rpms/x86_64
    println("urlFindRpms $url $rpmFilter $arch")
    def matches
    try {
        page = new URL(url).text
    } catch(java.io.IOException details) {
        println("Failed to get url $url")
        return []
    }
    // Look for rpmFilter-ed rpms on base/link/arch/ page
    matches = page =~ Pattern.compile("href=\"($rpmFilter[^\"]*(noarch|$arch)\\.rpm)\"[^>]*>[^<]+<")
    if (matches.size() > 0) {
        // Links found, translate relative path and report it
        links = []
        matches.each {link ->
            links.add(new URL(new URL(url), link[1]).toString())
        }
        return links
    }
    // No RPM pkgs found, check if arch link is available
    matches = page =~ Pattern.compile("href=\"([^\"]+)\"[^>]*>$arch/?<")
    for (match in matches) {
        urlTarget = new URL(new URL(url), match[1]).toString()
        links = urlFindRpms(urlTarget, rpmFilter, arch)
        if (links.size() > 0) {
            return links
        }
    }
    // No matches in any $arch link
    return []
}

@NonCPS
String cmdInstallRpmsFromURLs(String param, String arch) {
    // Wrapper to run urlFindLinksToRpms on jenkins params
    //
    // The param format is:
    // $pkgFilter;$rpmFilter;$urlList\n
    // $urlList\n
    // ...
    //
    // Where $pkgFilter and $rpmFilter is Java regular expression or
    // one can use '!foo|bar|baz in order to match anything but the
    // passed items (translates into "(?!.*(foo|bar|baz))")
    allLinks = []
    for (String line in param.split('\n')) {
        args = line.split("(?<!\\\\);")
        if (args.size() == 1) {
            // Only $urlList specified
            links = urlFindLinksToRpms(args[0], '', '', arch)
        } else if (args.size() == 3) {
            // $urlList, $pkgFilter and $rpmFilter specified
            for (i in [0, 1]) {
                if (args[i].startsWith("!")) {
                    // Add simplification for inverse match
                    args[i] = '(?!.*(' + args[i][1 .. -1] + '))'
                }
            }
            links = urlFindLinksToRpms(args[2], args[0], args[1], arch)
        } else {
            println("Incorrect parameter ${line}")
            continue
        }
        if (links.size() > 0) {
            for (link in links) {
                allLinks.add(link.replace('\n', ''))
            }
        } else {
            println("No matches for $line")
        }
    }
    if (allLinks.size() > 0) {
        return 'dnf install -y --allowerasing --skip-broken ' + allLinks.join(' ')
    }
    return ''
}

@NonCPS
List urlFindLinksToRpms(String urlList, String pkgFilter='', String rpmFilter='', String arch='') {
    // Searches html page and it's links for links to RPMs based on the filters
    //
    // The filters to find RPMs is ">${rpmFilter}.*($arch|noarch).rpm<"
    // and it searches (urlList: https://example.com/rpms):
    // 1. the provided $urlList page
    //    - eg: https://example.com/rpms
    // 2. all pages linked from $urlList page using "$arch/?" filter
    //    - eg: https://example.com/rpms/x86_64
    // 3. all pages linked from $urlList page using $pkgFilter filter
    //    - eg: https://example.com/rpms/2023-01-01
    // 4. all pages linkef from the $pkgFilter-ed pages using "$arch/?" filter
    //    - eg: https://example.com/rpms/2023-01-01/x86_64
    //
    // pkgFilter/rpmFilter uses Java regular expression, you can use things like
    // 'kernel' to match "^kernel-XYZ"
    // '[^\"]*kernel' to match "whateverkernel-XYZ"
    // '(?!.*(debug|doc))[^\"]*extra' to match "whatever-extra-whatever" that does
    //     not contain "debug", nor "doc"
    println("urlFindLinksToRpms $urlList $pkgFilter $rpmFilter $arch")
    def matches
    // First try looking for RPMs directly on this page
    links = urlFindRpms(urlList, rpmFilter, arch)
    if (links.size() > 0) {
        return links
    }
    try {
        page = new URL(urlList).text
    } catch(java.io.IOException details) {
        println("Failed to get url $urlList")
        return []
    }
    // Look for pkgFilter-ed links
    matches = page =~ Pattern.compile("href=\"([^\"]+)\"[^>]*>$pkgFilter[^<]*<")
    for (match in matches) {
        urlTarget = new URL(new URL(urlList), match[1]).toString()
        links = urlFindRpms(urlTarget, rpmFilter, arch)
        if (links) {
            return links
        }
    }
    return []
}

List getLatestDistros(String name, Integer limit, String arch) {
    // Return latest $limit distros matching the name (use % to match anything)
    println("getLatestDistros $name")
    distros = sh(returnStdout: true,
                 script: ('echo -n $(bkr distro-trees-list --arch  ' + arch + ' --name=' + name +
                          ' --limit ' + limit + bkrExtraArgs + ' --format json ' +
                          '| grep \'"distro_name"\' | cut -d\'"\' -f4)'
                         )).trim().split()
    return(distros)
}

@NonCPS
List getTestedDistros(String jobName, String distro) {
    // Turn our distro (RHEL-8.0.0-20000000.n.0) into regex (RHEL-d.d.d-dddddddd.n.d)
    // (this is unsafe method that leaves the '.' and such, but should do for now)
    reNum = '[0-9]'
    reDistro = distro.replaceAll(reNum, reNum)
    reDistro = reDistro.replaceAll('%', '[^ ]*')
    distros = []
    for (build in Jenkins.instance.getJob(jobName).builds) {
        build?.description?.eachMatch(reDistro) {
            dist -> distros.add(dist)
        }
    }
    return(distros)
}

String getLatestUntestedDistro(String distro, String arch) {
    // Return latest distro that has not been tested by this job yet
    tested_distros = getTestedDistros(env.JOB_NAME, distro)
    latest_distros = getLatestDistros(distro, 10, arch)
    for (dist in latest_distros) {
        if (!(dist in tested_distros)) {
            return(dist)
        }
    }
    failBuild('No untested distros to try',
              "All past 10 distros were already tested ${latest_distros}")
    return("")
}

List getDistroRange(String[] range, String workerNode, String arch) {
    // Wrapper to allow "..".split() as well as ["foo", "bar"]]
    return(getDistroRange(range.toList(), workerNode, arch))
}

List getDistroRange(List range, String workerNode, String arch) {
    // Find all distros between range[0] and range[1] revision (max 100 versions)
    println("getDistroRange ${range}")
    first = range[0]
    last = range[1]
    common = ''
    for (i = 0; i < Math.min(first.length(), last.length()); i++) {
        if (first[i] != last[i]) {
            break
        }
        common += first[i]
    }
    if (first.contains('n') && last.contains('n')) {
        common += '%n'
    } else if (first.contains('d') && last.contains('d')) {
        common += '%d'
    }
    node(workerNode) {
        distros = getLatestDistros(common + '%', 100, arch).reverse();
        distroRange = [];
        i = 0;
        while (i < distros.size()) {
            if (distros[i] == first) {
                break;
            }
            ++i;
        }
        while (i < distros.size()) {
            distroRange.add(distros[i]);
            if (distros[i++] == last) {
                break;
            }
        }
    }
    return(distroRange)
}

List getDistrosRange(String[] range, String workerNode, String arch) {
    // Wrapper to allow "..".split() as well as ["foo", "bar"]]
    return(getDistrosRange(range.toList(), workerNode, arch))
}

List getDistrosRange(List distrosRaw, String workerNode, String arch) {
    // Process list of distros and replace '..' ranges with individual versions
    println("getDistrosRange ${distrosRaw}")
    List distros = []
    for (distro in distrosRaw) {
        if (distro.contains('..')) {
            distroRange = getDistroRange(distro.split('\\.\\.'), workerNode, arch)
            println("range ${distroRange}")
            distros += distroRange.toList()
        } else {
            println("add ${distro}")
            distros.add(distro)
        }
    }
    return(distros)
}

@NonCPS
List getGoodBuildNumbers(String jobName) {
    // Build is non-serializable object, we have to use NonCPS
    // on the other hand we can not use copyArtifacts inside NonCPS
    // therefore we have to only query for all descriptions and
    // then iterate throught them, because we don't know how many
    // builds we are going to need (copyArtifacts can fail)
    builds = []
    for (build in Jenkins.instance.getJob(jobName).builds) {
        if (build?.description?.startsWith('BAD')) {
            println("skip ${build.description} ${build.number}")
        } else {
            builds.add(build.number)
            if (build?.description?.startsWith('STOP')) {
                print("stop processing, STOP build detected ${build.description} ${build.number}")
                break
            }
        }
    }
    return builds
}

String setupScript(output, kernelArgs, rpmFromURLs, arch, fioNbdSetup) {
    // Generate additional parts of the setup script
    if (kernelArgs) {
        output += "\ngrubby --args '${kernelArgs}' --update-kernel=\$(grubby --default-kernel)"
    }
    // Ugly way of installing all arch's rpms from a site, allowing a filter
    // this is usually used on koji/brew to allow updating certain packages
    // warning: It does not work when the url rpm is older.
    if (rpmFromURLs) {
        output += cmdInstallRpmsFromURLs(rpmFromURLs, arch)
    }
    // Install deps and compile custom fio with nbd ioengine
    if (fioNbdSetup) {
        output += fioNbdScript
    }
    output += '\n'
    return output
}

@NonCPS
def triggerRunperf(String jobName, Boolean waitForStart, String distro, String guestDistro,
                   String machine, String arch, String tests, String profiles, String srcBuild,
                   String hostKernelArgs, String hostRpmFromURLs,
                   String guestKernelArgs, String guestRpmFromURLs,
                   String upstreamQemuCommit, String descriptionPrefix,
                   Boolean pbenchPublish, Boolean fioNbdSetup, String noReferenceBuilds,
                   String cmpModelJob, String cmpModelBuild, String cmpTolerance,
                   String cmpStddevTolerance, String githubPublisherProject, String hostScript,
                   String workerScript) {
    // Trigger a run-perf job setting all of the params according to arguments
    // on waitForStart returns the triggered build.id, otherwise it
    // returns srcBuild value
    parameters = [
        new StringParameterValue('DISTRO', distro),
        new StringParameterValue('GUEST_DISTRO', guestDistro),
        new StringParameterValue('MACHINE', machine),
        new StringParameterValue('ARCH', arch),
        new StringParameterValue('TESTS', tests),
        new StringParameterValue('PROFILES', profiles),
        new StringParameterValue('SRC_BUILD', srcBuild),
        new StringParameterValue('HOST_KERNEL_ARGS', hostKernelArgs),
        new TextParameterValue('HOST_RPM_FROM_URLS', hostRpmFromURLs),
        new StringParameterValue('GUEST_KERNEL_ARGS', guestKernelArgs),
        new TextParameterValue('GUEST_RPM_FROM_URLS', guestRpmFromURLs),
        new StringParameterValue('UPSTREAM_QEMU_COMMIT', upstreamQemuCommit),
        new StringParameterValue('DESCRIPTION_PREFIX', descriptionPrefix),
        new BooleanParameterValue('PBENCH_PUBLISH', pbenchPublish),
        new BooleanParameterValue('FIO_NBD_SETUP', fioNbdSetup),
        new StringParameterValue('NO_REFERENCE_BUILDS', noReferenceBuilds),
        new StringParameterValue('CMP_MODEL_JOB', cmpModelJob),
        new StringParameterValue('CMP_MODEL_BUILD', cmpModelBuild),
        new StringParameterValue('CMP_TOLERANCE', cmpTolerance),
        new StringParameterValue('CMP_STDDEV_TOLERANCE', cmpStddevTolerance),
        new StringParameterValue('GITHUB_PUBLISHER_PROJECT', githubPublisherProject),
        new TextParameterValue('HOST_SCRIPT', hostScript),
        new TextParameterValue('WORKER_SCRIPT', workerScript)
        ]
    job = Hudson.instance.getJob(jobName)
    queue = job.scheduleBuild2(0, new ParametersAction(parameters))
    if (waitForStart) {
        println('Waiting for build to be scheduled to obtain srcBuild ID')
        build = queue.waitForStart()
        srcBuild = "${build.id}"
    }
    // Explicitly clean build, job and queue, otherwise we get CPS failures
    build = job = queue = null
    return(srcBuild)
}

@NonCPS
void tryOtherDistros(String rawDistro, String arch) {
    // Re-trigger the job with another untested distro if possible
    String strProvisionFail = 'Provisioning failed'
    if (!rawDistro.startsWith('latest-')) {
        // Using strict distro version
        failBuild(strProvisionFail,
                  "Provisioning failed, bailing out as we are using strict distro ${rawDistro}")
    }
    if (rawDistro.startsWith('latest-untested-')) {
        latestDistro = rawDistro
    } else {
        latestDistro = "latest-untested-${rawDistro[7..-1]}"
    }
    triggerRunperf(env.JOB_NAME, false, latestDistro, params.GUEST_DISTRO, params.MACHINE, params.ARCH,
                   params.TESTS, params.PROFILES, params.SRC_BUILD, params.HOST_KERNEL_ARGS,
                   params.HOST_RPM_FROM_URLS, params.GUEST_KERNEL_ARGS,
                   params.GUEST_RPM_FROM_URLS, params.UPSTREAM_QEMU_COMMIT,
                   params.DESCRIPTION_PREFIX, params.PBENCH_PUBLISH, params.FIO_NBD_SETUP,
                   params.NO_REFERENCE_BUILDS, params.CMP_MODEL_JOB, params.CMP_MODEL_BUILD,
                   params.CMP_TOLERANCE, params.CMP_STDDEV_TOLERANCE, params.GITHUB_PUBLISHER_PROJECT,
                   params.HOST_SCRIPT, params.WORKER_SCRIPT)
}

@NonCPS
def getBuildEnv(String jobName, String buildName) {
    env = Hudson.instance.getJob(jobName).getBuildByNumber(buildName as int).getEnvironment()
    str_env = "${env}"
    env = null
    return str_env
}

into $YOUR_SHARED_LIB_REPO/vars/runperf.groovy and you should be good to go. Feel free to ping me if anything goes wrong.