├── .github └── workflows │ ├── codeql.yml │ ├── image-trigger.yml │ ├── naughty-prune.yml │ └── test.yml ├── .gitignore ├── HACKING.md ├── LICENSE ├── README.md ├── checkout-and-run ├── cockpit-lib-update ├── github-info ├── github-upload-secrets ├── image-create ├── image-customize ├── image-diff ├── image-download ├── image-prune ├── image-refresh ├── image-trigger ├── image-upload ├── images ├── alpine ├── alpine-efi ├── arch ├── centos-10 ├── centos-9-bootc ├── centos-9-stream ├── cirros ├── debian-stable ├── debian-testing ├── fedora-41 ├── fedora-42 ├── fedora-coreos ├── fedora-rawhide ├── fedora-rawhide-anaconda-payload ├── fedora-rawhide-boot ├── fedora-rawhide-live-boot ├── files │ └── ca.pem ├── opensuse-tumbleweed ├── rhel-10-0 ├── rhel-10-1 ├── rhel-8-10 ├── rhel-8-8 ├── rhel-9-2 ├── rhel-9-4 ├── rhel-9-6 ├── rhel-9-7 ├── scripts │ ├── alpine-efi.bootstrap │ ├── alpine-efi.setup │ ├── alpine.bootstrap │ ├── alpine.setup │ ├── arch.bootstrap │ ├── arch.setup │ ├── bootc.setup │ ├── centos-10.bootstrap │ ├── centos-10.setup │ ├── centos-9-bootc.bootstrap │ ├── centos-9-bootc.setup │ ├── centos-9-stream.bootstrap │ ├── centos-9-stream.setup │ ├── cirros.bootstrap │ ├── create-anaconda-payload │ ├── debian-stable.bootstrap │ ├── debian-stable.setup │ ├── debian-testing.bootstrap │ ├── debian-testing.setup │ ├── debian.setup │ ├── fedora-41.bootstrap │ ├── fedora-41.setup │ ├── fedora-42.bootstrap │ ├── fedora-42.setup │ ├── fedora-coreos.bootstrap │ ├── fedora-coreos.setup │ ├── fedora-rawhide-anaconda-payload.bootstrap │ ├── fedora-rawhide-boot.bootstrap │ ├── fedora-rawhide-live-boot.bootstrap │ ├── fedora-rawhide.bootstrap │ ├── fedora-rawhide.setup │ ├── fedora.setup │ ├── foonux.bootstrap │ ├── lib │ │ ├── bootc.Containerfile │ │ ├── bootc.bootstrap │ │ ├── build-deps.sh │ │ ├── cloudimage.bootstrap │ │ ├── cockpit-ci.fcc │ │ ├── cockpit-ci.ign │ │ ├── make-srpm │ │ ├── mcast1.nmconnection │ │ ├── podman-images.setup │ │ ├── pubring.gpg │ │ ├── secring.gpg │ │ └── zero-disk.setup │ ├── opensuse-tumbleweed.bootstrap │ ├── opensuse-tumbleweed.setup │ ├── ostree.setup │ ├── rhel-10-0.bootstrap │ ├── rhel-10-0.setup │ ├── rhel-10-1.bootstrap │ ├── rhel-10-1.setup │ ├── rhel-8-10.bootstrap │ ├── rhel-8-10.setup │ ├── rhel-8-8.bootstrap │ ├── rhel-8-8.setup │ ├── rhel-9-2.bootstrap │ ├── rhel-9-2.setup │ ├── rhel-9-4.bootstrap │ ├── rhel-9-4.setup │ ├── rhel-9-6.bootstrap │ ├── rhel-9-6.setup │ ├── rhel-9-7.bootstrap │ ├── rhel-9-7.setup │ ├── rhel.setup │ ├── services.bootstrap │ ├── services.setup │ ├── ubuntu-2204.bootstrap │ ├── ubuntu-2204.setup │ ├── ubuntu-2404.bootstrap │ ├── ubuntu-2404.setup │ ├── ubuntu-stable.bootstrap │ └── ubuntu-stable.setup ├── services ├── ubuntu-2204 ├── ubuntu-2404 └── ubuntu-stable ├── inspect-queue ├── issue-scan ├── issues-review ├── job-runner ├── job-runner.toml ├── lib ├── __init__.py ├── aio │ ├── __init__.py │ ├── base.py │ ├── github.py │ ├── job.py │ ├── jobcontext.py │ ├── jsonutil.py │ ├── local.py │ ├── s3.py │ ├── s3streamer.py │ ├── spawn.py │ └── util.py ├── allowlist.py ├── constants.py ├── directories.py ├── jobqueue.py ├── network.py ├── py.typed ├── s3-html │ └── log.html ├── s3.py ├── stores.py └── testmap.py ├── machine ├── __init__.py ├── cloud-init.iso ├── host_key ├── host_key.pub ├── identity ├── identity.pub ├── machine_core │ ├── __init__.py │ ├── cli.py │ ├── exceptions.py │ ├── machine.py │ ├── machine_virtual.py │ ├── ssh_connection.py │ ├── testvm.py │ └── timeout.py ├── make-cloud-init-iso ├── py.typed └── testvm.py ├── naughty-prune ├── naughty ├── arch │ ├── 4796-stratis-runs-clevis-too-early │ ├── 5090-lvm2-resize-ntfs-unexpected-error │ ├── 5090-lvresize-fails-with-stratis-signature │ ├── 7646-libvirt-attach-disk-segfault │ └── 7648-libvirt-unable-restore-snapshot ├── centos-10 ├── centos-8-stream ├── centos-9-bootc ├── centos-9-stream ├── debian-stable │ ├── 2463-no-pod-events │ ├── 2463-no-pod-events-1 │ ├── 2463-no-pod-events-2 │ └── 2485-ipa-leave-crash ├── debian-testing │ ├── 2463-no-pod-events │ ├── 2463-no-pod-events-1 │ ├── 2463-no-pod-events-2 │ ├── 2485-ipa-leave-crash │ ├── 5364-apparmor-sysfs-zoned │ ├── 7646-libvirt-attach-disk-segfault │ └── 7648-libvirt-unable-restore-snapshot ├── example │ ├── 123-log-and-traceback │ └── 9876-example-traceback ├── fedora-41 │ ├── 3683-selinux-agetty-clhm │ ├── 4796-stratis-runs-clevis-too-early │ ├── 6678-selinux-libvirt-ssh │ ├── 6769-kdump-initramfs-unpack-error │ ├── 6992-firefox-hidden-canvas-bug │ ├── 7629-kdump-initrd-generation │ ├── 7631-stratis-crypto-pool-boot │ ├── 7716-checkpoint-restore-failure │ ├── 7765-kdump-ansible-crashkernel-size │ ├── 7765-kdump-crashkernel-size │ └── 7765-kdump-crashkernel-size-2 ├── fedora-42 │ ├── 3683-selinux-agetty-clhm │ ├── 4796-stratis-runs-clevis-too-early │ ├── 6678-selinux-libvirt-ssh │ ├── 6769-kdump-initramfs-unpack-error │ ├── 6992-firefox-hidden-canvas-bug │ ├── 7629-kdump-initrd-generation │ ├── 7629-kdump-initrd-generation-2 │ ├── 7629-kdump-initrd-generation-3 │ ├── 7631-stratis-crypto-pool-boot │ ├── 7716-checkpoint-restore-failure │ ├── 7765-kdump-ansible-crashkernel-size │ ├── 7765-kdump-crashkernel-size │ └── 7765-kdump-crashkernel-size-2 ├── fedora-43 │ ├── 3683-selinux-agetty-clhm │ ├── 4796-stratis-runs-clevis-too-early │ ├── 6678-selinux-libvirt-ssh │ ├── 6769-kdump-initramfs-unpack-error │ ├── 6992-firefox-hidden-canvas-bug │ ├── 7648-libvirt-unable-restore-snapshot │ ├── 7707-blivet-parted-disk-not-found │ └── 7716-checkpoint-restore-failure ├── fedora-coreos ├── fedora-rawhide ├── fedora-rawhide-boot ├── opensuse-tumbleweed │ ├── 7648-libvirt-unable-restore-snapshot │ └── 7700-qemuBlockThrottleFiltersDetach-crash ├── rhel-10-0 ├── rhel-10-1 ├── rhel-10 │ ├── 2538-iso-over-https │ ├── 3683-selinux-agetty-clhm │ ├── 4796-stratis-runs-clevis-too-early │ ├── 6678-selinux-libvirt-ssh │ ├── 7629-kdump-initrd-generation │ ├── 7648-libvirt-unable-restore-snapshot │ ├── 7700-qemuBlockThrottleFiltersDetach-crash │ ├── 7716-checkpoint-restore-failure │ ├── 7765-kdump-crashkernel-size │ └── 7765-kdump-crashkernel-size-2 ├── rhel-8-10 ├── rhel-8-8 ├── rhel-8 │ ├── 1374-libvirt-crashes-on-test-teardown │ ├── 2412-unlocking-stratis-during-boot │ └── 4796-stratis-runs-clevis-too-early ├── rhel-9-2 ├── rhel-9-4 ├── rhel-9-6 ├── rhel-9-7 ├── rhel-9 │ ├── 2538-iso-over-https │ ├── 3683-selinux-agetty-clhm │ ├── 4796-stratis-runs-clevis-too-early │ ├── 4796-stratis-runs-clevis-too-early-old │ ├── 5090-lvresize-fails-with-stratis-signature │ ├── 6769-kdump-initramfs-unpack-error │ ├── 7374-selinux-nmmeta │ ├── 7765-kdump-crashkernel-size │ └── 7765-kdump-crashkernel-size-2 ├── ubuntu-2204 │ ├── 2463-no-pod-events │ ├── 2463-no-pod-events-1 │ ├── 2463-no-pod-events-2 │ ├── 2485-ipa-leave-crash │ ├── 4829-podman-hang │ ├── 4829-podman-hang-2 │ ├── 4829-podman-hang-3 │ ├── 4829-podman-hang-4 │ ├── 4829-podman-hang-5 │ └── 4829-podman-hang-6 ├── ubuntu-2404 │ ├── 2485-ipa-leave-crash │ ├── 5364-apparmor-sysfs-zoned │ └── 7432-netman-vs-netplan └── ubuntu-stable │ ├── 2485-ipa-leave-crash │ ├── 5364-apparmor-sysfs-zoned │ ├── 7432-netman-vs-netplan │ └── 7692-criu-errors ├── npm ├── npm-update ├── po-refresh ├── prometheus-stats ├── publish-queue ├── push-rewrite ├── pyproject.toml ├── recreate-dependabot-pr ├── run-queue ├── s3-lifecycle ├── setup-deploy-keys ├── setup-deploy-keys-anaconda ├── store-tests ├── task ├── __init__.py ├── cache.py ├── distributed_queue.py ├── github.py └── test_mock_server.py ├── tasks-container-update ├── test-failure-policy ├── test ├── run ├── test_aio.py ├── test_cache.py ├── test_checklist.py ├── test_github.py ├── test_issue_scan.py ├── test_task.py ├── test_test_failure_policy.py ├── test_testmap.py └── test_tests_scan.py ├── tests-scan ├── tests-status ├── tests-trigger ├── tests.html ├── vm-reset └── vm-run /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: CodeQL 2 | on: 3 | pull_request: 4 | push: 5 | branches: [main] 6 | 7 | jobs: 8 | analyze: 9 | name: Analyze 10 | runs-on: ubuntu-latest 11 | permissions: 12 | actions: read 13 | contents: read 14 | security-events: write 15 | 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | language: [ javascript, python ] 20 | 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@v4 24 | 25 | - name: Initialize CodeQL 26 | uses: github/codeql-action/init@v2 27 | with: 28 | languages: ${{ matrix.language }} 29 | queries: +security-and-quality 30 | 31 | - name: Autobuild 32 | uses: github/codeql-action/autobuild@v2 33 | if: ${{ matrix.language == 'javascript' || matrix.language == 'python' }} 34 | 35 | - name: Perform CodeQL Analysis 36 | uses: github/codeql-action/analyze@v2 37 | with: 38 | category: "/language:${{ matrix.language }}" 39 | -------------------------------------------------------------------------------- /.github/workflows/image-trigger.yml: -------------------------------------------------------------------------------- 1 | name: image refresh trigger 2 | on: 3 | schedule: 4 | # this is UTC-4 5 | - cron: '30 22 * * *' 6 | # can be run manually on https://github.com/cockpit-project/bots/actions 7 | workflow_dispatch: 8 | jobs: 9 | maintenance: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Set up secrets 13 | run: echo '${{ secrets.GITHUB_TOKEN }}' > ~/.config/github-token 14 | 15 | - name: Clone repository 16 | uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | 20 | - name: Trigger image refreshes 21 | run: ./image-trigger 22 | -------------------------------------------------------------------------------- /.github/workflows/naughty-prune.yml: -------------------------------------------------------------------------------- 1 | name: prune naughties 2 | on: 3 | schedule: 4 | - cron: '30 1 * * 0' 5 | # can be run manually on https://github.com/cockpit-project/bots/actions 6 | workflow_dispatch: 7 | jobs: 8 | maintenance: 9 | runs-on: ubuntu-latest 10 | environment: self 11 | permissions: 12 | issues: read 13 | pull-requests: write 14 | statuses: write 15 | steps: 16 | - name: Set up secrets 17 | run: echo '${{ secrets.GITHUB_TOKEN }}' > ~/.config/github-token 18 | 19 | - name: Clone repository 20 | uses: actions/checkout@v4 21 | with: 22 | ssh-key: ${{ secrets.DEPLOY_KEY }} 23 | fetch-depth: 0 24 | 25 | - name: Run naughty-prune 26 | run: | 27 | git config --global user.name "GitHub Workflow" 28 | git config --global user.email "cockpituous@cockpit-project.org" 29 | mkdir -p ~/.config/cockpit-dev 30 | echo ${{ github.token }} >> ~/.config/cockpit-dev/github-token 31 | ./naughty-prune --verbose 32 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | on: [pull_request] 3 | jobs: 4 | bots: 5 | runs-on: ubuntu-22.04 6 | container: 7 | image: ghcr.io/cockpit-project/tasks 8 | options: --user root 9 | permissions: 10 | pull-requests: none 11 | steps: 12 | - name: Clone repository 13 | uses: actions/checkout@v4 14 | 15 | # https://github.blog/2022-04-12-git-security-vulnerability-announced/ 16 | - name: Pacify git's permission check 17 | run: git config --global --add safe.directory /__w/bots/bots 18 | 19 | - name: Run test 20 | run: test/run 21 | 22 | cockpituous: 23 | runs-on: ubuntu-22.04 24 | permissions: 25 | # enough permissions for tests-scan to work 26 | pull-requests: read 27 | statuses: write 28 | steps: 29 | - name: Clone repository 30 | uses: actions/checkout@v4 31 | with: 32 | # need this to get origin/main for git diff 33 | fetch-depth: 0 34 | 35 | - name: Rebase to target branch 36 | run: | 37 | git config user.name github-actions 38 | git config user.email github-actions@github.com 39 | git rebase origin/${{ github.event.pull_request.base.ref }} 40 | 41 | - name: Check whether there are changes that might affect the deployment 42 | id: changes 43 | run: | 44 | git log --exit-code --stat HEAD --not origin/${{ github.event.pull_request.base.ref }} -- \ 45 | ':!.github/workflows' \ 46 | ':!README.md' \ 47 | ':!HACKING.md' \ 48 | ':!images' \ 49 | ':!image-create' \ 50 | ':!image-customize' \ 51 | ':!image-trigger' \ 52 | ':!naughty' \ 53 | ':!machine/machine_core' \ 54 | ':!lib/allowlist.py' \ 55 | ':!lib/testmap.py' \ 56 | ':!test/' \ 57 | ':!vm-run' \ 58 | >&2 || echo "changed=true" >> "$GITHUB_OUTPUT" 59 | 60 | - name: Ensure branch was proposed from origin 61 | if: steps.changes.outputs.changed 62 | run: test "${{ github.event.pull_request.head.repo.url }}" = "${{ github.event.pull_request.base.repo.url }}" 63 | 64 | - name: Clone cockpituous repository 65 | if: steps.changes.outputs.changed 66 | uses: actions/checkout@v4 67 | with: 68 | repository: cockpit-project/cockpituous 69 | path: cockpituous 70 | 71 | - name: Install test dependencies 72 | if: steps.changes.outputs.changed 73 | run: | 74 | sudo apt-get update 75 | sudo apt-get install -y make python3-pytest 76 | 77 | # HACK: Ubuntu 22.04 has podman 3.4, which isn't compatible with podman-remote 4 in our tasks container 78 | # This PPA is a backport of podman 4.3 from Debian 12; drop this when moving `runs-on:` to ubuntu-24.04 79 | - name: Update to newer podman 80 | if: steps.changes.outputs.changed 81 | run: | 82 | sudo add-apt-repository -y ppa:quarckster/containers 83 | sudo apt install -y podman 84 | systemctl --user daemon-reload 85 | 86 | - name: Test local CI deployment 87 | if: steps.changes.outputs.changed 88 | run: | 89 | set -ex 90 | if [ -n '${{ github.event.pull_request.number }}' ]; then 91 | echo '${{ secrets.GITHUB_TOKEN }}' > /tmp/github-token 92 | pr_args='--pr-repository ${{ github.event.pull_request.base.user.login }}/bots --pr ${{ github.event.pull_request.number }} --github-token=/tmp/github-token' 93 | repo='${{ github.event.pull_request.head.repo.clone_url }}' 94 | branch='${{ github.event.pull_request.head.ref }}' 95 | else 96 | # push event; skip testing a PR 97 | repo='${{ github.event.repository.clone_url }}' 98 | branch="${GITHUB_REF##*/}" 99 | fi 100 | cd cockpituous 101 | COCKPIT_BOTS_REPO=$repo COCKPIT_BOTS_BRANCH=$branch python3 -m pytest -vv ${pr_args:-} 102 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iso 2 | *.partial 3 | *.pyc 4 | *.qcow2 5 | *.tar.?? 6 | *.xz 7 | *~ 8 | /*.log 9 | /build-results/ 10 | make-checkout-workdir 11 | -------------------------------------------------------------------------------- /HACKING.md: -------------------------------------------------------------------------------- 1 | # Hacking on the Cockpit Bots 2 | 3 | Most bots are python scripts. Shared code is in the tasks/ directory. 4 | 5 | ## Environment 6 | 7 | The bots work in containers that are built in the [cockpituous](https://github.com/cockpit-project/cockpituous) 8 | repository. New dependencies should be added there in the `tasks/container/Containerfile` 9 | file in that repository. 10 | 11 | ## Bots filing issues 12 | 13 | Many bots file or work with issues in GitHub repository. We can use issues to tell 14 | bots what to do. Often certan bots will just file issues for tasks that are outstanding. 15 | And in many cases other bots will then perform those tasks. 16 | 17 | These bots are listed in the `./issue-scan` file. They are written using the 18 | `tasks/__init__.py` code. These are deprecated in favor of GitHub workflows. 19 | 20 | ## Bots printing output 21 | 22 | The bots which run on our own infrastructure post their output into the 23 | requesting GitHub issue. This currently only applies to `image-refresh`, all 24 | other bots run in GitHub actions. 25 | 26 | ## Contributing to bots 27 | 28 | Development of the bots happens on GitHub at https://github.com/cockpit-project/bots/ 29 | 30 | There are static code and syntax checks which you should run often: 31 | 32 | test/run 33 | 34 | You will need to either use the tasks container to run this script or install: 35 | 36 | * python3-mypy 37 | * python3-pytest 38 | * python3-aioresponses 39 | * python3-aiohttp 40 | * ruff 41 | 42 | It is highly recommended to set this up as a git pre-push hook, to avoid 43 | pushing PRs that will fail on trivial errors: 44 | 45 | ln -s ../../test/run .git/hooks/pre-push 46 | 47 | ### Updating pixel tests code 48 | 49 | > [!NOTE] 50 | > This will only update the `log.html` page and redirect all links there to the log URL set in ``. For `pixeldiff.html` there is currently no written dev guide. 51 | 52 | 53 | * Easiest way to develop is to go to `./lib/s3-html/log.html` and within `` add a test URL for what you want to improve layout for. 54 | ``html 55 | 56 | 57 | ``` 58 | * Start a server for the `lib/` directory with `python -m http.server -d ./lib/s3-html` 59 | * Open up the URL echoed in terminal and go to `/log.html` 60 | * Make changes in `log.html` and see changes refresh live in the browser 61 | -------------------------------------------------------------------------------- /checkout-and-run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # NB: This file gets run inside of a container with nothing else present. It 4 | # needs to depend on only the Python standard library. 5 | 6 | import argparse 7 | import os 8 | import shlex 9 | import subprocess 10 | import sys 11 | import time 12 | from collections.abc import Sequence 13 | 14 | 15 | def log(*parts: object) -> None: 16 | print(*parts, file=sys.stderr) 17 | 18 | 19 | def run(*cmd: str, tries: int = 1) -> None: 20 | joined = shlex.join(cmd) 21 | 22 | for attempt in range(1, tries + 1): 23 | try: 24 | log('\n+', joined) 25 | subprocess.check_call(cmd) 26 | return 27 | except subprocess.CalledProcessError as exc: 28 | log(f'\n> Attempt {attempt} failed with code {exc.returncode}.') 29 | time.sleep(2 ** attempt) 30 | 31 | sys.exit(f'\n*** Failed to run command {joined}. Aborting.') 32 | 33 | 34 | def output(*cmd: str) -> str: 35 | try: 36 | return subprocess.check_output(cmd, text=True).strip() 37 | except subprocess.CalledProcessError as exc: 38 | sys.exit(f'\n*** Failed to run command {shlex.join(cmd)} (code {exc.returncode}). Aborting.') 39 | 40 | 41 | def git(*cmd: str, tries: int = 1) -> None: 42 | run('git', *cmd, tries=tries) 43 | 44 | 45 | def git_output(*cmd: str) -> str: 46 | return output('git', *cmd) 47 | 48 | 49 | def checkout_and_run(repository: str, revision: str | None, rebase: str | None, command: Sequence[str]) -> None: 50 | run('cat', '/run/.containerenv') 51 | run('uname', '-a') 52 | run('mkdir', '-p', os.environ['TEST_ATTACHMENTS']) 53 | 54 | git('clone', '--', repository, 'make-checkout-workdir', tries=5) 55 | 56 | print('\n+ cd make-checkout-workdir', file=sys.stderr) 57 | os.chdir('make-checkout-workdir') 58 | 59 | if revision: 60 | git('fetch', '--', 'origin', revision, tries=5) 61 | git('checkout', '--detach', 'FETCH_HEAD') 62 | 63 | if rebase: 64 | git('fetch', '--', 'origin', rebase, tries=5) 65 | # Do it this way to get the commit ID in the log 66 | base = git_output('rev-parse', 'FETCH_HEAD') 67 | git('rebase', '--', base) 68 | 69 | git('log', 'HEAD', f'^{base}') 70 | git('log', '-n1', base) 71 | else: 72 | git('log', '-n1') 73 | 74 | commands: Sequence[Sequence[str]] 75 | if command: 76 | commands = (command,) 77 | else: 78 | commands = ( 79 | ('.cockpit-ci/run',), 80 | ('test/run',) 81 | ) 82 | 83 | for cmd in commands: 84 | try: 85 | log('\n+', shlex.join(cmd)) 86 | os.execv(cmd[0], tuple(cmd)) 87 | except FileNotFoundError as exc: 88 | log(exc) 89 | except OSError as exc: 90 | log(exc) 91 | break 92 | 93 | sys.exit('\n*** Failed to execute entry point.') 94 | 95 | 96 | def main() -> None: 97 | # All output to stdout — otherwise podman reorders things 98 | os.dup2(1, 2) 99 | 100 | parser = argparse.ArgumentParser(description="Check out a git repo and run a command; mainly used by job-runner") 101 | parser.add_argument('--revision', help="The revision to checkout") 102 | parser.add_argument('--rebase', help="Target branch to rebase onto") 103 | parser.add_argument('repository', help="The git repository to clone") 104 | parser.add_argument('command', nargs='*', help="The command to run [default: .cockpit-ci/run]") 105 | 106 | args = parser.parse_args() 107 | log('\n+ [checkout-and-run]', shlex.join(sys.argv[1:])) 108 | 109 | checkout_and_run(args.repository, args.revision, args.rebase, args.command) 110 | 111 | 112 | if __name__ == '__main__': 113 | main() 114 | -------------------------------------------------------------------------------- /cockpit-lib-update: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # This file is part of Cockpit. 4 | # 5 | # Copyright (C) 2023 Red Hat, Inc. 6 | # 7 | # Cockpit is free software; you can redistribute it and/or modify it 8 | # under the terms of the GNU Lesser General Public License as published by 9 | # the Free Software Foundation; either version 2.1 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # Cockpit is distributed in the hope that it will be useful, but 13 | # WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 15 | # Lesser General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Lesser General Public License 18 | # along with Cockpit; If not, see . 19 | 20 | # Update COCKPIT_REPO_COMMIT to cockpit HEAD automatically, defaults to 21 | # Makefile as input optionally the full path can be provided. (For example 22 | # Anaconda uses ui/webui/Makefile.am). 23 | 24 | import os 25 | import re 26 | import subprocess 27 | import sys 28 | import tempfile 29 | from pathlib import Path 30 | 31 | import task 32 | from lib.constants import BASE_DIR 33 | 34 | sys.dont_write_bytecode = True 35 | 36 | GIT_URL_RE = r'COCKPIT_REPO_URL\s*=\s*(.*)' 37 | GIT_COMMIT_RE = r'COCKPIT_REPO_COMMIT\s*=\s*(.*)' 38 | 39 | 40 | def run(context, verbose=False, **kwargs): 41 | cockpit_repo_url = 'https://github.com/cockpit-project/cockpit.git' 42 | cockpit_repo_commit = 'HEAD' 43 | makefile = context or 'Makefile' 44 | makefile_path = os.path.join(BASE_DIR, makefile) 45 | 46 | with open(makefile_path) as fp: 47 | content = fp.read() 48 | 49 | m = re.search(GIT_URL_RE, content) 50 | if m: 51 | cockpit_repo_url = m.group(1) 52 | 53 | m = re.search(GIT_COMMIT_RE, content) 54 | if m: 55 | cockpit_repo_commit = m.group(1) 56 | 57 | # Figure out latest cockpit tip commit 58 | with tempfile.TemporaryDirectory('cockpit-repo') as tmpdir: 59 | tmpdir = Path(tmpdir) 60 | clone_dir = 'cockpit' 61 | commit = cockpit_repo_commit.partition('#')[0].strip() 62 | subprocess.check_call(['git', 'clone', cockpit_repo_url, clone_dir], cwd=tmpdir) 63 | git_describe = subprocess.check_output(['git', 'describe'], cwd=tmpdir / clone_dir).decode().strip() 64 | git_head = subprocess.check_output(['git', 'rev-parse', 'HEAD'], cwd=tmpdir / clone_dir).decode().strip() 65 | git_shortlog = subprocess.check_output(['git', 'shortlog', f'{commit}...', '--', 66 | 'pkg/lib', 'test/common', 'test/static-code', 'tools/node-modules'], 67 | cwd=tmpdir / clone_dir).decode().strip() 68 | 69 | try: 70 | # when HEAD is not tagged, this looks like "290-9-g4a6d86f5b" 71 | tag, commits, _ = git_describe.split('-') 72 | comment = f'{git_head} # {tag} + {commits} commits' 73 | except ValueError: 74 | # when HEAD is tagged, use that name 75 | comment = f'{git_head} # {git_describe}' 76 | 77 | new_content = content.replace(cockpit_repo_commit, comment) 78 | if content == new_content: 79 | print("COCKPIT_REPO_COMMIT is already up to date, nothing to do") 80 | return 81 | 82 | with open(makefile_path, 'w') as fp: 83 | fp.write(new_content) 84 | 85 | title = f"Makefile: Update Cockpit lib to {git_head[:32]}" 86 | branch = task.branch('cockpit-lib', title, pathspec=makefile, **kwargs) 87 | kwargs["title"] = title 88 | kwargs["body"] = git_shortlog 89 | task.pull(branch, **kwargs) 90 | 91 | 92 | if __name__ == '__main__': 93 | task.main(function=run, title="Update COCKPIT_REPO_COMMIT for cockpit projects") 94 | -------------------------------------------------------------------------------- /github-info: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # This file is part of Cockpit. 4 | # 5 | # Copyright (C) 2015 Red Hat, Inc. 6 | # 7 | # Cockpit is free software; you can redistribute it and/or modify it 8 | # under the terms of the GNU Lesser General Public License as published by 9 | # the Free Software Foundation; either version 2.1 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # Cockpit is distributed in the hope that it will be useful, but 13 | # WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 15 | # Lesser General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Lesser General Public License 18 | # along with Cockpit; If not, see . 19 | 20 | # Shared GitHub code. When run as a script, we print out info about 21 | # our GitHub interaction. 22 | 23 | import argparse 24 | import datetime 25 | import sys 26 | 27 | from task import github 28 | 29 | sys.dont_write_bytecode = True 30 | 31 | 32 | def httpdate(dt: datetime.datetime) -> str: 33 | """Return a string representation of a date according to RFC 1123 34 | (HTTP/1.1). 35 | 36 | The supplied date must be in UTC. 37 | 38 | From: http://stackoverflow.com/a/225106 39 | 40 | """ 41 | weekday = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"][dt.weekday()] 42 | month = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", 43 | "Oct", "Nov", "Dec"][dt.month - 1] 44 | return "%s, %02d %s %04d %02d:%02d:%02d GMT" % (weekday, dt.day, month, 45 | dt.year, dt.hour, dt.minute, dt.second) 46 | 47 | 48 | def main() -> int: 49 | parser = argparse.ArgumentParser(description='Test GitHub rate limits') 50 | parser.parse_args() 51 | 52 | # in order for the limit not to be affected by the call itself, 53 | # use a conditional request with a timestamp in the future 54 | 55 | future_timestamp = datetime.datetime.now(tz=datetime.UTC) + datetime.timedelta(seconds=3600) 56 | 57 | api = github.GitHub() 58 | headers = {'If-Modified-Since': httpdate(future_timestamp)} 59 | response = api.request("GET", "git/refs/heads/main", "", headers) 60 | sys.stdout.write("Rate limits:\n") 61 | for entry in ["X-RateLimit-Limit", "X-RateLimit-Remaining", "X-RateLimit-Reset"]: 62 | entries = [t for t in response['headers'].items() if t[0].lower() == entry.lower()] 63 | if entries: 64 | if entry == "X-RateLimit-Reset": 65 | readable = datetime.datetime.fromtimestamp(float(entries[0][1]), tz=datetime.UTC).isoformat() 66 | sys.stdout.write(f"{entry}: {entries[0][1]} ({readable})\n") 67 | else: 68 | sys.stdout.write(f"{entry}: {entries[0][1]}\n") 69 | 70 | return 0 71 | 72 | 73 | if __name__ == '__main__': 74 | sys.exit(main()) 75 | -------------------------------------------------------------------------------- /image-diff: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # This file is part of Cockpit. 4 | # 5 | # Copyright (C) 2020 Red Hat, Inc. 6 | # 7 | # Cockpit is free software; you can redistribute it and/or modify it 8 | # under the terms of the GNU Lesser General Public License as published by 9 | # the Free Software Foundation; either version 2.1 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # Cockpit is distributed in the hope that it will be useful, but 13 | # WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 15 | # Lesser General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Lesser General Public License 18 | # along with Cockpit; If not, see . 19 | 20 | import argparse 21 | from collections.abc import Mapping 22 | 23 | from machine import testvm 24 | 25 | 26 | def get_packages(machine: testvm.VirtMachine) -> Mapping[str, str]: 27 | # List all packages, irrespective of package manager (rpm or dpkg or pacman). 28 | # If both are missing, then the command will fail. 29 | # 30 | # We'd ideally like to get source packages everywhere, but it's a 31 | # bit more difficult on RPM. (TODO) 32 | pkgcmd = """if type dpkg-query > /dev/null 2>&1; then 33 | dpkg-query -W 2>/dev/null; 34 | elif type rpm > /dev/null 2>&1; then 35 | rpm -qa --qf '%{NAME}\t%{EVR}\n' 2>/dev/null; 36 | else pacman -Q | sed 's/ /\t/' 2>/dev/null; fi""" 37 | 38 | output = machine.execute(pkgcmd).strip() 39 | return dict(line.split('\t') for line in output.splitlines()) 40 | 41 | 42 | parser = argparse.ArgumentParser(description='Compare package versions on VM images') 43 | parser.add_argument('old', help='the "old" image to compare') 44 | parser.add_argument('new', help='the "new" image to compare') 45 | args = parser.parse_args() 46 | 47 | # boot the machines in parallel 48 | old_vm = testvm.VirtMachine(image=args.old) 49 | new_vm = testvm.VirtMachine(image=args.new) 50 | 51 | old_vm.start() 52 | new_vm.start() 53 | 54 | old_vm.wait_boot() 55 | new_vm.wait_boot() 56 | 57 | old_pkgs = get_packages(old_vm) 58 | new_pkgs = get_packages(new_vm) 59 | 60 | old_vm.kill() 61 | new_vm.kill() 62 | 63 | print('Removed:') 64 | for name in sorted(set(old_pkgs) - set(new_pkgs)): 65 | print(f' {name} ({old_pkgs[name]})') 66 | print() 67 | 68 | print('Added:') 69 | for name in sorted(set(new_pkgs) - set(old_pkgs)): 70 | print(f' {name} ({new_pkgs[name]})') 71 | print() 72 | 73 | print('Changed:') 74 | for name in sorted(set.intersection(set(new_pkgs), set(old_pkgs))): 75 | if new_pkgs[name] != old_pkgs[name]: 76 | print(f' {name} ({old_pkgs[name]} -> {new_pkgs[name]})') 77 | print() 78 | -------------------------------------------------------------------------------- /image-refresh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # This file is part of Cockpit. 4 | # 5 | # Copyright (C) 2016-2024 Red Hat, Inc. 6 | # 7 | # Cockpit is free software; you can redistribute it and/or modify it 8 | # under the terms of the GNU Lesser General Public License as published by 9 | # the Free Software Foundation; either version 2.1 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # Cockpit is distributed in the hope that it will be useful, but 13 | # WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 15 | # Lesser General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Lesser General Public License 18 | # along with Cockpit; If not, see . 19 | 20 | import json 21 | import os 22 | import shlex 23 | import subprocess 24 | import sys 25 | from typing import Any 26 | 27 | import task 28 | from lib import testmap 29 | from lib.constants import BOTS_DIR, SCRIPTS_DIR 30 | 31 | sys.dont_write_bytecode = True 32 | 33 | 34 | def run(*cmd: str, check: bool = True, **kwargs: Any) -> int: 35 | print('\n+', shlex.join(cmd), file=sys.stderr) 36 | result = subprocess.run(cmd, **kwargs, check=check) 37 | return result.returncode 38 | 39 | 40 | def with_logs(n: int, log_url: str | None) -> int | tuple[int, str]: 41 | if log_url: 42 | return n, log_url 43 | return n 44 | 45 | 46 | def image_refresh(image: str, **kwargs: Any) -> int | tuple[int, str]: 47 | try: 48 | log_url = os.environ.get('COCKPIT_CI_LOG_URL') 49 | 50 | dry_run: bool = kwargs['dry'] 51 | triggers = testmap.tests_for_image(image) 52 | 53 | # Cleanup any extraneous disk usage elsewhere 54 | run('./vm-reset') 55 | 56 | # download the current image, for comparing them; that may not exist yet for newly introduced images 57 | if run('./image-download', image, check=False) == 0: 58 | old_image = os.path.realpath(f'{BOTS_DIR}/images/{image}') 59 | else: 60 | old_image = None 61 | 62 | # create the new image 63 | run('./image-create', '--verbose', image, 64 | env={**os.environ, 'VIRT_BUILDER_NO_CACHE': "yes"}) 65 | 66 | # upload the new image 67 | if dry_run: 68 | task.would('./image-upload', '--prune-s3', image) 69 | else: 70 | run('./image-upload', '--prune-s3', image) 71 | 72 | # compare it to the previous one (on hosts we can ssh to) 73 | if old_image and os.path.exists(f'{SCRIPTS_DIR}/{image}.setup'): 74 | run('./image-diff', old_image, image) 75 | 76 | # create branch and push it 77 | branch = task.branch(image, f"images: Update {image} image", pathspec="images", **kwargs) 78 | 79 | # trigger tests if it is not a pull request 80 | if branch and "pull" not in kwargs: 81 | pull = task.pull(branch, labels=['bot', 'no-test'], run_tests=False, **kwargs) 82 | 83 | if log_url: 84 | # Create a synthetic status for the log URL 85 | log_status = { 86 | 'state': 'success', 87 | 'context': f'image-refresh/{image}', 88 | 'description': 'Forwarded status', 89 | 'target_url': log_url 90 | } 91 | if dry_run: 92 | task.would('add status', json.dumps(log_status, indent=4)) 93 | else: 94 | task.api.post(f"statuses/{pull['head']['sha']}", log_status) 95 | 96 | # Trigger this pull request 97 | if dry_run: 98 | task.would('trigger tests:', json.dumps(triggers, indent=4)) 99 | else: 100 | head = pull["head"]["sha"] 101 | for trigger in triggers: 102 | task.api.post(f"statuses/{head}", { 103 | "state": "pending", 104 | "context": trigger, 105 | "description": task.github.NOT_TESTED_DIRECT 106 | }) 107 | 108 | except subprocess.CalledProcessError as exc: 109 | return with_logs(exc.returncode, log_url) 110 | else: 111 | return with_logs(0, log_url) 112 | 113 | 114 | if __name__ == '__main__': 115 | task.main(function=image_refresh, title="Refresh image") 116 | -------------------------------------------------------------------------------- /image-trigger: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # This file is part of Cockpit. 4 | # 5 | # Copyright (C) 2015 Red Hat, Inc. 6 | # 7 | # Cockpit is free software; you can redistribute it and/or modify it 8 | # under the terms of the GNU Lesser General Public License as published by 9 | # the Free Software Foundation; either version 2.1 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # Cockpit is distributed in the hope that it will be useful, but 13 | # WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 15 | # Lesser General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Lesser General Public License 18 | # along with Cockpit; If not, see . 19 | 20 | import argparse 21 | import os 22 | import random 23 | import subprocess 24 | import sys 25 | import time 26 | 27 | import task 28 | from lib.constants import BASE_DIR 29 | 30 | sys.dont_write_bytecode = True 31 | 32 | # default for refresh-days 33 | DAYS = 7 34 | 35 | REFRESH_30 = {"refresh-days": 30} 36 | 37 | # stable/old OSes don't need to be refreshed as often 38 | REFRESH = { 39 | "arch": {}, 40 | "centos-9-bootc": {}, 41 | "centos-9-stream": {}, 42 | "centos-10": {}, 43 | "debian-testing": {}, 44 | "debian-stable": {}, 45 | "fedora-41": {}, 46 | "fedora-42": {}, 47 | "fedora-coreos": {}, 48 | "fedora-rawhide": {}, 49 | "fedora-rawhide-boot": {}, 50 | "fedora-rawhide-anaconda-payload": REFRESH_30, 51 | "fedora-rawhide-live-boot": REFRESH_30, 52 | "opensuse-tumbleweed": {}, 53 | "ubuntu-2204": {}, 54 | "ubuntu-2404": {}, 55 | "ubuntu-stable": {}, 56 | "rhel-8-8": REFRESH_30, 57 | "rhel-8-10": {}, 58 | "rhel-9-2": REFRESH_30, 59 | "rhel-9-4": {}, 60 | "rhel-9-6": {}, 61 | "rhel-9-7": {}, 62 | "rhel-10-0": {}, 63 | "rhel-10-1": {}, 64 | "services": REFRESH_30, 65 | } 66 | 67 | 68 | def main(): 69 | parser = argparse.ArgumentParser(description='Ensure necessary issue exists for image refresh') 70 | parser.add_argument('-v', '--verbose', action="store_true", default=False, 71 | help="Print verbose information") 72 | parser.add_argument("image", nargs="?") 73 | opts = parser.parse_args() 74 | api = task.github.GitHub() 75 | 76 | try: 77 | scan(api, opts.image, opts.verbose) 78 | except RuntimeError as ex: 79 | sys.stderr.write("image-trigger: " + str(ex) + "\n") 80 | return 1 81 | 82 | return 0 83 | 84 | 85 | # Check if the given files that match @pathspec are stale 86 | # and haven't been updated in @days. 87 | def stale(days, pathspec, ref="HEAD", verbose=False): 88 | def execute(*args): 89 | if verbose: 90 | sys.stderr.write("+ " + " ".join(args) + "\n") 91 | output = subprocess.check_output(args, cwd=BASE_DIR, text=True) 92 | if verbose: 93 | sys.stderr.write("> " + output + "\n") 94 | return output 95 | 96 | timestamp = execute("git", "log", "--max-count=1", "--pretty=format:%ct", ref, "--", pathspec) 97 | try: 98 | timestamp = int(timestamp) 99 | except ValueError: 100 | timestamp = 0 101 | 102 | # We randomize when we think this should happen over a day 103 | offset = days * 86400 104 | due = time.time() - random.randint(offset - 43200, offset + 43200) 105 | 106 | return timestamp < due 107 | 108 | 109 | def scan(api, force, verbose): 110 | subprocess.check_call(["git", "fetch", "origin", "main"]) 111 | for (image, options) in REFRESH.items(): 112 | perform = False 113 | 114 | if force: 115 | perform = image == force 116 | else: 117 | days = options.get("refresh-days", DAYS) 118 | perform = stale(days, os.path.join("images", image), "origin/main", verbose) 119 | 120 | if perform: 121 | text = f"Image refresh for {image}" 122 | issue = task.issue(text, text, "image-refresh", image) 123 | sys.stderr.write(f'#{issue["number"]}: image-refresh {image}\n') 124 | 125 | 126 | if __name__ == '__main__': 127 | sys.exit(main()) 128 | -------------------------------------------------------------------------------- /images/alpine: -------------------------------------------------------------------------------- 1 | alpine-597d8648c1e78169b009dc7b5dcfebb47e6c3158394e858bfe2b788949d9cec9.qcow2 -------------------------------------------------------------------------------- /images/alpine-efi: -------------------------------------------------------------------------------- 1 | alpine-efi-77de78210bf38e5ebbd8b65dc4fc6394e0c1e1e4f3736197dc63fe139e6fc35c.qcow2 -------------------------------------------------------------------------------- /images/arch: -------------------------------------------------------------------------------- 1 | arch-5ed217bf3b26d10cda9ef488128f706af973830c81eb45eff959e444e4b2334d.qcow2 -------------------------------------------------------------------------------- /images/centos-10: -------------------------------------------------------------------------------- 1 | centos-10-c9e126939dc4263b3f4fbb01ced58a0b0e257dfad9d2feeeb826a87afa76c774.qcow2 -------------------------------------------------------------------------------- /images/centos-9-bootc: -------------------------------------------------------------------------------- 1 | centos-9-bootc-29d4f8ed26819c96f8906df3a749343e330c5adc8b163aaa5e5ba862d8b235b9.qcow2 -------------------------------------------------------------------------------- /images/centos-9-stream: -------------------------------------------------------------------------------- 1 | centos-9-stream-d1ca46a2ce131e260e87f2968939c6529b41e675aa6229388f9969a87597a6cd.qcow2 -------------------------------------------------------------------------------- /images/cirros: -------------------------------------------------------------------------------- 1 | cirros-ff4ccf16a162d7d3bf86d30141bd8cfe30821dd3b09712fe2f84d201c8e948af.qcow2 -------------------------------------------------------------------------------- /images/debian-stable: -------------------------------------------------------------------------------- 1 | debian-stable-4334b52890bfd3123444d0bffb258db653da55d59718d347debfcc283c44c5db.qcow2 -------------------------------------------------------------------------------- /images/debian-testing: -------------------------------------------------------------------------------- 1 | debian-testing-57d13742b6edfbcb1e742ab83bca91b0a48bc31e01cc3a59327f2c107aba5b72.qcow2 -------------------------------------------------------------------------------- /images/fedora-41: -------------------------------------------------------------------------------- 1 | fedora-41-b7368fce5a1ee5aa1760939043e78b5d0834d90a0ba10154e0b87a7bd1ae7223.qcow2 -------------------------------------------------------------------------------- /images/fedora-42: -------------------------------------------------------------------------------- 1 | fedora-42-248824de4eb91467efe68d8c857d6e6318e8b93df90ba5c77af7c44344e5c673.qcow2 -------------------------------------------------------------------------------- /images/fedora-coreos: -------------------------------------------------------------------------------- 1 | fedora-coreos-a3e9933c39e6495c44ac90b757049895da1617e01dabc1417fc31bbd7200e0c7.qcow2 -------------------------------------------------------------------------------- /images/fedora-rawhide: -------------------------------------------------------------------------------- 1 | fedora-rawhide-449f01688272dca578e68d73f659aa1aee2aa2dbb342fe7c0eee69f0d11ac296.qcow2 -------------------------------------------------------------------------------- /images/fedora-rawhide-anaconda-payload: -------------------------------------------------------------------------------- 1 | fedora-rawhide-anaconda-payload-60c7aeeacb034bf8ceada824fa41c073461c1d3beb7ec400cefa1d3b29608cd0.tar.gz -------------------------------------------------------------------------------- /images/fedora-rawhide-boot: -------------------------------------------------------------------------------- 1 | fedora-rawhide-boot-422c32b2d3336b51cc979543d75116dda7df784a4ebdc495215ee16f4ca5def9.iso -------------------------------------------------------------------------------- /images/fedora-rawhide-live-boot: -------------------------------------------------------------------------------- 1 | fedora-rawhide-live-boot-2ef9d5e78c3e9fd6469532ae0a28b5e7421b770916e4e559b2155492d6783f45.iso -------------------------------------------------------------------------------- /images/files/ca.pem: -------------------------------------------------------------------------------- 1 | # This is the CA for cockpit-tests images and data 2 | 3 | -----BEGIN CERTIFICATE----- 4 | MIIDOTCCAiGgAwIBAgIUPpZnkFZx1YOolUyBBJeaFZ1WMwIwDQYJKoZIhvcNAQEL 5 | BQAwNTEQMA4GA1UECgwHQ29ja3BpdDEUMBIGA1UECwwLQ29ja3BpdHVvdXMxCzAJ 6 | BgNVBAMMAkNBMCAXDTI0MTIxNjE0MTUxOFoYDzMwMjQwNDE4MTQxNTE4WjA1MRAw 7 | DgYDVQQKDAdDb2NrcGl0MRQwEgYDVQQLDAtDb2NrcGl0dW91czELMAkGA1UEAwwC 8 | Q0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDaFPWA0jyyDJypEdrP 9 | BUS3Bwsllj5sCvlvIJnoMFbR9F3XfKDOFmaVnX3/9prCQOnjZApfT0RUX3ythyUD 10 | NrbNVDrqVZ7mRHgWXSSwmgUdG+5tuGI0W+cQlfUhqWGGpQaXX8G7CbAGBR3u8USR 11 | jgEa1oaXTbdIhmTSvSRtpua22Jioi5VT8B1j+A5bstl3wJEYtbboklTuSUIq85nq 12 | p9DSBLOs3RGZFv13gIt9lA0SFnBil8QOnrKjb8n1BRgVJll1/ur8jB0sHrfzWq7w 13 | eSGf6kZktzp+rl6ZpVHWggiR1JEOPDGE5uzcDLvBpwGIah/VCOoy8LhFusELC1te 14 | vW+tAgMBAAGjPzA9MA8GA1UdEwEB/wQFMAMBAf8wCwYDVR0PBAQDAgEGMB0GA1Ud 15 | DgQWBBQbsujdjWQiwZUZK5SmHRdsxrvRVzANBgkqhkiG9w0BAQsFAAOCAQEAaHqZ 16 | JTM8xx3vkFF69gGI3xUi6ezpvKMySJ52QhECQvAs1KPa8lydy/WqPmBBKU14kDKd 17 | U1hcsNHY7pz+60LmsaUgro6J9CHrFOEsS3KJm3QUhoj+qOxRn+W/v5BbQ74J2SIY 18 | 8qjD7Vox9ai4vhJ9G7KICHYVZUsLOVho2EzwS9uLPQoDAAUDVwp0gCuMAuDDpHDP 19 | kEF/xAkVeXgwfxhjdLiS+dTltXfIwcNa+AIEmv+NZfkfZ3pN76dg95l8fyj6FYi0 20 | 4jCmZkQFWfUambnlJcGm24SLWNprGF09hnCRd+mNX7g2raII1x6vIsyPq21QC/1D 21 | 4J5EPTkYmwRurqhAlA== 22 | -----END CERTIFICATE----- 23 | -------------------------------------------------------------------------------- /images/opensuse-tumbleweed: -------------------------------------------------------------------------------- 1 | opensuse-tumbleweed-cb3466e29d04d9acd782df5ce1232e07c9914b6e22a0861a729a08f4fd0a8b4c.qcow2 -------------------------------------------------------------------------------- /images/rhel-10-0: -------------------------------------------------------------------------------- 1 | rhel-10-0-2e2abb1013030af3f44db532b04448986b1c1197ee29546150e7fd9f11090b92.qcow2 -------------------------------------------------------------------------------- /images/rhel-10-1: -------------------------------------------------------------------------------- 1 | rhel-10-1-3a5fc33c89580d9acbf3b688a42cce47853702c76a4794ab858bf2abb60e0cb6.qcow2 -------------------------------------------------------------------------------- /images/rhel-8-10: -------------------------------------------------------------------------------- 1 | rhel-8-10-fe1dcd975ffe75fcf44882a66c696cb76969cd536b2c1d352cdcf38b6305b8f8.qcow2 -------------------------------------------------------------------------------- /images/rhel-8-8: -------------------------------------------------------------------------------- 1 | rhel-8-8-e3a7d7329106fcb53ee498d9a9b1184ab571087839f230a5efbeddcaf80cace1.qcow2 -------------------------------------------------------------------------------- /images/rhel-9-2: -------------------------------------------------------------------------------- 1 | rhel-9-2-048fb26307378877807597d75d11226ace2da707bbc01e7c44ead669e80e3413.qcow2 -------------------------------------------------------------------------------- /images/rhel-9-4: -------------------------------------------------------------------------------- 1 | rhel-9-4-4be007e0273f36f1629f45a167d253980760ea256e9aa5f0542ca20ea5bf8dd3.qcow2 -------------------------------------------------------------------------------- /images/rhel-9-6: -------------------------------------------------------------------------------- 1 | rhel-9-6-926c3bf6411c14f13c2e2464e5807d971f7970a87c482576e38cf25c75a690be.qcow2 -------------------------------------------------------------------------------- /images/rhel-9-7: -------------------------------------------------------------------------------- 1 | rhel-9-7-b9b930392846c4dfc90a44cad2ea154a35b93fb5f59061fb4d7a9f04347058f7.qcow2 -------------------------------------------------------------------------------- /images/scripts/alpine-efi.bootstrap: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright (C) 2024 Red Hat Inc. 4 | # 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, but 11 | # WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | # General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program; if not, write to the Free Software 17 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 18 | # 02110-1301 USA. 19 | 20 | set -eux 21 | 22 | URL='https://dl-cdn.alpinelinux.org/alpine/v3.19/releases/cloud/nocloud_alpine-3.19.1-x86_64-uefi-cloudinit-r0.qcow2' 23 | exec $(dirname $0)/lib/cloudimage.bootstrap "$1" "$URL" "+0M" 24 | -------------------------------------------------------------------------------- /images/scripts/alpine-efi.setup: -------------------------------------------------------------------------------- 1 | alpine.setup -------------------------------------------------------------------------------- /images/scripts/alpine.bootstrap: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright (C) 2023 Red Hat Inc. 4 | # 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, but 11 | # WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | # General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program; if not, write to the Free Software 17 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 18 | # 02110-1301 USA. 19 | 20 | set -eux 21 | 22 | URL='https://dl-cdn.alpinelinux.org/alpine/v3.19/releases/cloud/nocloud_alpine-3.19.1-x86_64-bios-cloudinit-r0.qcow2' 23 | exec $(dirname $0)/lib/cloudimage.bootstrap "$1" "$URL" "+0M" 24 | -------------------------------------------------------------------------------- /images/scripts/alpine.setup: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | set -eux 4 | 5 | apk add qemu-guest-agent 6 | apk del cloud-init 7 | 8 | rc-update add qemu-guest-agent default 9 | rc-update del chronyd default 10 | -------------------------------------------------------------------------------- /images/scripts/arch.bootstrap: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright (C) 2021 Red Hat Inc. 4 | # 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, but 11 | # WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | # General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program; if not, write to the Free Software 17 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 18 | # 02110-1301 USA. 19 | 20 | set -eux 21 | 22 | URL='https://america.mirror.pkgbuild.com/images/latest/' 23 | IMAGE="$(curl -L -s "$URL" | grep -o '"Arch-Linux-x86_64-cloudimg-[^"]*.qcow2"' | tr -d '"' | tail -n1)" 24 | [ -n "$IMAGE" ] 25 | 26 | exec $(dirname $0)/lib/cloudimage.bootstrap "$1" "$URL/$IMAGE" 27 | -------------------------------------------------------------------------------- /images/scripts/bootc.setup: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -eux 3 | 4 | IMAGE="$1" 5 | 6 | # config.json cannot set GECOS 7 | usermod -c Administrator admin 8 | 9 | podman pull quay.io/cockpit/ws 10 | podman pull quay.io/jitesoft/nginx 11 | 12 | # for c-podman tests 13 | /var/lib/testvm/podman-images.setup 14 | 15 | # store our own OCI image into a local registry, for c-ostree tests 16 | podman load < /var/cache/bootc.oci.tar 17 | 18 | mkdir /var/lib/cockpit-test-registry 19 | chcon -t container_file_t /var/lib/cockpit-test-registry/ 20 | podman run -d --rm --name ostree-registry -p 5000:5000 -v /var/lib/cockpit-test-registry:/var/lib/registry localhost/test-registry 21 | mv /etc/containers/registries.conf /etc/containers/registries.conf.orig 22 | printf '[registries.insecure]\nregistries = ["localhost:5000"]\n' > /etc/containers/registries.conf 23 | 24 | podman tag localhost/bootc:latest localhost:5000/bootc:latest 25 | podman push localhost:5000/bootc:latest 26 | podman rmi localhost:5000/bootc:latest localhost/bootc:latest 27 | podman rm -f -t0 ostree-registry 28 | rm /var/cache/bootc.oci.tar 29 | 30 | # disable various maintenance tasks which interfere with tests and don't make sense for our tests 31 | systemctl disable bootc-fetch-apply-updates.timer fstrim.timer logrotate.timer raid-check.timer 32 | 33 | # reduce image size 34 | /var/lib/testvm/zero-disk.setup 35 | -------------------------------------------------------------------------------- /images/scripts/centos-10.bootstrap: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | set -eux 3 | 4 | URL="https://cloud.centos.org/centos/10-stream/x86_64/images/CentOS-Stream-GenericCloud-10-latest.x86_64.qcow2" 5 | exec $(dirname $0)/lib/cloudimage.bootstrap "$1" "$URL" 6 | -------------------------------------------------------------------------------- /images/scripts/centos-10.setup: -------------------------------------------------------------------------------- 1 | rhel.setup -------------------------------------------------------------------------------- /images/scripts/centos-9-bootc.bootstrap: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eux 3 | exec $(dirname $0)/lib/bootc.bootstrap "$1" quay.io/centos-bootc/centos-bootc:stream9 4 | -------------------------------------------------------------------------------- /images/scripts/centos-9-bootc.setup: -------------------------------------------------------------------------------- 1 | bootc.setup -------------------------------------------------------------------------------- /images/scripts/centos-9-stream.bootstrap: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | set -eux 3 | 4 | URL='https://cloud.centos.org/centos/9-stream/x86_64/images/CentOS-Stream-GenericCloud-9-latest.x86_64.qcow2' 5 | 6 | exec $(dirname $0)/lib/cloudimage.bootstrap "$1" "$URL" 7 | -------------------------------------------------------------------------------- /images/scripts/centos-9-stream.setup: -------------------------------------------------------------------------------- 1 | rhel.setup -------------------------------------------------------------------------------- /images/scripts/cirros.bootstrap: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -eux 3 | 4 | OUTPUT="$1" 5 | 6 | curl -L https://download.cirros-cloud.net/0.4.0/cirros-0.4.0-i386-disk.img > "$OUTPUT" 7 | 8 | # prepare a cloud-init iso for disabling network source, to avoid a 90s timeout at boot 9 | WORKDIR=$(mktemp -d) 10 | trap "rm -rf '$WORKDIR'" EXIT INT QUIT PIPE 11 | cd "$WORKDIR" 12 | 13 | cat > meta-data < user-data <. 18 | 19 | # create-anaconda-payload -- Create a payload to be used by anaconda installer tests. 20 | 21 | import argparse 22 | import os 23 | import subprocess 24 | 25 | from lib.constants import BOTS_DIR 26 | from machine import testvm 27 | 28 | KICKSTART = """\ 29 | cmdline 30 | timezone Europe/Prague --utc 31 | keyboard --vckeymap=us --xlayouts='us' 32 | lang en_US.UTF-8 33 | url --url https://dl.fedoraproject.org/pub/fedora/linux/development/rawhide/Everything/x86_64/os/ 34 | rootpw test # root gets locked anyway 35 | %packages --excludedocs --exclude-weakdeps --inst-langs en 36 | openssh 37 | xfsprogs 38 | exfatprogs 39 | e2fsprogs 40 | efibootmgr 41 | grub2-tools 42 | grub2-pc 43 | grub2-pc-modules 44 | grub2-tools-efi 45 | grub2-tools-extra 46 | grub2-efi-x64 47 | grubby 48 | shim-x64 49 | cryptsetup 50 | btrfs-progs 51 | mdadm 52 | lvm2 53 | %end 54 | """ 55 | 56 | KICKSTART_PATH = "/tmp/payload.ks" 57 | 58 | 59 | def build_payload(image: str, output: str) -> None: 60 | subprocess.check_call([os.path.join(BOTS_DIR, "image-download"), image]) 61 | machine = testvm.VirtMachine(image=image, memory_mb=4096) 62 | try: 63 | machine.start() 64 | machine.wait_boot() 65 | machine.execute("dnf install -y anaconda", timeout=300) 66 | 67 | # Create directory /mnt/sysimage and start installation 68 | machine.write(KICKSTART_PATH, KICKSTART) 69 | machine.execute( 70 | f"mkdir -p /mnt/sysimage && anaconda --kickstart {KICKSTART_PATH} --dirinstall /mnt/sysimage", 71 | timeout=600 72 | ) 73 | 74 | # Change directory to /mnt/sysimage/ and create archive 75 | machine.execute("cd /mnt/sysimage && tar --selinux --acls --xattrs -zcvf /root/payload.tar.gz *", timeout=100) 76 | 77 | machine.download("/root/payload.tar.gz", output) 78 | finally: 79 | machine.stop() 80 | 81 | 82 | def main() -> None: 83 | parser = argparse.ArgumentParser() 84 | parser.add_argument('--image', default='fedora-rawhide') 85 | parser.add_argument('--output', required=True) 86 | args = parser.parse_args() 87 | 88 | if not args.output: 89 | raise RuntimeError("Output path not specified") 90 | 91 | build_payload(args.image, args.output) 92 | 93 | 94 | main() 95 | -------------------------------------------------------------------------------- /images/scripts/debian-stable.bootstrap: -------------------------------------------------------------------------------- 1 | #! /bin/sh -ex 2 | 3 | RELEASE=bookworm 4 | RELEASENUM=12 5 | LATEST_DAILY=$(curl -s https://cloud.debian.org/images/cloud/$RELEASE/daily/ | sed -n '/ "$CACHE" 13 | xz -cd "$CACHE" > "$OUTPUT" 14 | 15 | # boot it once to run ignition 16 | qemu-system-x86_64 -enable-kvm -nographic -m 1024 -device virtio-rng-pci \ 17 | -drive file="$OUTPUT",if=virtio -fw_cfg name=opt/com.coreos/config,file=$BASE/lib/cockpit-ci.ign 18 | -------------------------------------------------------------------------------- /images/scripts/fedora-coreos.setup: -------------------------------------------------------------------------------- 1 | ostree.setup -------------------------------------------------------------------------------- /images/scripts/fedora-rawhide-anaconda-payload.bootstrap: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -eux 3 | 4 | OUTPUT="$1" 5 | 6 | PYTHONPATH=. python3 images/scripts/create-anaconda-payload --output=$OUTPUT 7 | -------------------------------------------------------------------------------- /images/scripts/fedora-rawhide-boot.bootstrap: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -eux 3 | 4 | OUTPUT="$1" 5 | 6 | URL='https://download.fedoraproject.org/pub/fedora/linux/development/rawhide/Server/x86_64/os/images/boot.iso' 7 | 8 | curl -L "$URL" -o "$OUTPUT" 9 | -------------------------------------------------------------------------------- /images/scripts/fedora-rawhide-live-boot.bootstrap: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -eux 3 | 4 | OUTPUT="$1" 5 | 6 | ISO_FOLDER='https://download.fedoraproject.org/pub/fedora/linux/development/rawhide/Workstation/x86_64/iso' 7 | ISO=$(curl -L --silent https://download.fedoraproject.org/pub/fedora/linux/development/rawhide/Workstation/x86_64/iso/ | grep -oP 'href="\K[^"]+' | grep -E '\.iso' | head -n1 | tr -d '\n') 8 | URL="$ISO_FOLDER/$ISO" 9 | 10 | curl -L "$URL" -o "$OUTPUT" 11 | -------------------------------------------------------------------------------- /images/scripts/fedora-rawhide.bootstrap: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright (C) 2022 Red Hat Inc. 4 | # 5 | # This program is free software; you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation; either version 2 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, but 11 | # WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | # General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program; if not, write to the Free Software 17 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 18 | # 02110-1301 USA. 19 | 20 | set -eux 21 | 22 | URL='https://download.fedoraproject.org/pub/fedora/linux/development/rawhide/Cloud/x86_64/images/' 23 | IMAGE=$(curl -L -s "$URL" | grep -o 'Fedora-Cloud-Base-Generic[^"]*Rawhide[^"]*qcow2' | tr -d '"' | tail -n1) 24 | [ -n "$IMAGE" ] 25 | 26 | exec $(dirname $0)/lib/cloudimage.bootstrap "$1" "$URL/$IMAGE" 27 | -------------------------------------------------------------------------------- /images/scripts/fedora-rawhide.setup: -------------------------------------------------------------------------------- 1 | fedora.setup -------------------------------------------------------------------------------- /images/scripts/foonux.bootstrap: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # mock image which doesn't need any qemu/kvm; used by cockpituous integration tests 3 | echo fakeimage > $1 4 | date >> $1 5 | -------------------------------------------------------------------------------- /images/scripts/lib/bootc.Containerfile: -------------------------------------------------------------------------------- 1 | ARG base_image 2 | FROM $base_image 3 | 4 | # pre-install the distro version, which is useful for testing extensions and manual experiments 5 | # also pre-install ws and test dependencies 6 | # also install glib-networking, so that tests can install cockpit-ws (as long as it has that dependency) 7 | # also install tlog as a dependency needed for cockpit-session-recording 8 | RUN \ 9 | dnf update -y --exclude='kernel*' && \ 10 | dnf install -y --setopt install_weak_deps=False cockpit-system cockpit-networkmanager && \ 11 | dnf install -y dnsmasq pcp python3-pcp rsync sscg strace system-logos wireguard-tools && \ 12 | dnf install -y glib-networking && \ 13 | dnf install -y tlog && \ 14 | dnf clean all 15 | 16 | ADD lib/mcast1.nmconnection /usr/lib/NetworkManager/system-connections/ 17 | 18 | # NM insists on tight permissions 19 | RUN chmod 600 /usr/lib/NetworkManager/system-connections/mcast1.nmconnection 20 | 21 | # Make /usr/local writable for our testing: https://containers.github.io/bootc/filesystem.html#usrlocal 22 | RUN rm -rf /usr/local; ln -s ../var/usrlocal /usr/local 23 | -------------------------------------------------------------------------------- /images/scripts/lib/bootc.bootstrap: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # osbuild needs a privileged container with kvm, so we can't build or run this 4 | # directly in our CI tasks container or GitHub workflows; so run it in a VM. 5 | 6 | import argparse 7 | import json 8 | import os 9 | import subprocess 10 | import sys 11 | import tempfile 12 | from pathlib import Path 13 | 14 | sys.path.insert(1, os.path.realpath(__file__ + '/../../../..')) 15 | 16 | from lib.constants import BOTS_DIR, DEFAULT_IDENTITY_PUB_FILE, TEST_OS_DEFAULT 17 | from machine import testvm 18 | 19 | OCI_TAG = 'localhost/bootc:latest' 20 | 21 | parser = argparse.ArgumentParser(description='Bootstrap a bootc container based VM image') 22 | parser.add_argument('vmpath', type=Path, help='Path to the VM image to be created') 23 | parser.add_argument('container', help='Name of the bootc container image') 24 | args = parser.parse_args() 25 | 26 | subprocess.check_call([os.path.join(BOTS_DIR, 'image-download'), TEST_OS_DEFAULT]) 27 | m = testvm.VirtMachine(image=TEST_OS_DEFAULT, verbose=True) 28 | m.start() 29 | m.wait_boot() 30 | 31 | # build OS bootc container image 32 | m.upload([str(Path(testvm.SCRIPTS_DIR, 'lib'))], '/root') 33 | m.execute(f'podman build -f lib/bootc.Containerfile -t {OCI_TAG} --build-arg base_image={args.container} .', 34 | timeout=720, stdout=None) 35 | 36 | # convert container to qcow2 with image builder 37 | # see https://github.com/osbuild/bootc-image-builder?tab=readme-ov-file 38 | 39 | pubkey = Path(DEFAULT_IDENTITY_PUB_FILE).read_text().strip() 40 | config = { 41 | 'blueprint': { 42 | 'customizations': { 43 | 'user': [ 44 | {'name': 'root', 'password': 'foobar', 'key': pubkey}, 45 | {'name': 'admin', 'password': 'foobar', 'key': pubkey, 'groups': ['wheel']}, 46 | ] 47 | } 48 | } 49 | } 50 | m.write('/root/config.json', json.dumps(config)) 51 | 52 | m.execute('mkdir output') 53 | 54 | m.execute(['podman', 'run', '--rm', '-i', '--privileged', '--security-opt=label=type:unconfined_t', 55 | # for --local 56 | '--volume=/var/lib/containers/storage:/var/lib/containers/storage', 57 | '--volume=./config.json:/config.json', 58 | '--volume=./output:/output', 59 | 'quay.io/centos-bootc/bootc-image-builder:latest', 60 | # image-builder args 61 | '--type=qcow2', 62 | '--local', 63 | '--config', '/config.json', 64 | OCI_TAG], timeout=720, stdout=None) 65 | 66 | # copy out the converted qcow2 image 67 | m.download('output/qcow2/disk.qcow2', args.vmpath) 68 | 69 | # copy out the container image 70 | oci_image = tempfile.NamedTemporaryFile() 71 | m.execute(f"podman save {OCI_TAG}", stdout=oci_image) 72 | oci_image.flush() 73 | 74 | m.kill() 75 | 76 | # it's too small by default 77 | subprocess.check_call(['qemu-img', 'resize', '-f', 'qcow2', args.vmpath, '+20G']) 78 | 79 | # booting the image will cause bootc-generic-growpart to grow the /sysroot partition 80 | m = testvm.VirtMachine(image=os.path.abspath(args.vmpath), maintain=True, verbose=True) 81 | m.start() 82 | m.wait_boot() 83 | 84 | # copy OCI image into the VM as well, for cockpit-ostree tests 85 | # the .setup script processes this further 86 | m.upload([oci_image.name], '/var/cache/bootc.oci.tar') 87 | 88 | m.shutdown() 89 | m.kill() 90 | -------------------------------------------------------------------------------- /images/scripts/lib/build-deps.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Download cockpit.spec, replace `npm-version` macro and then query all build requires 4 | 5 | set -eu 6 | # Guard against GitHub outages, redirects etc., and let this script fail on rpmspec failures 7 | set -o pipefail 8 | 9 | GET="curl --silent --show-error --fail" 10 | COCKPIT_GIT="https://raw.githubusercontent.com/cockpit-project/cockpit" 11 | OS_VER="$1" 12 | # Remove variant information from OS_VER (e.g. fedora 40 eln -> fedora 40) 13 | OS_VER_NO_VARIANT="$(echo $OS_VER | cut -d' ' -f 1,2)" 14 | 15 | # most images use cockpit.spec from main branch, but stable RHEL branches diverge 16 | case "$OS_VER" in 17 | rhel*8|centos*8) 18 | spec=$($GET "$COCKPIT_GIT/rhel-8/tools/cockpit.spec") 19 | ;; 20 | *suse*) 21 | # macro for determining suse version is %suse_version 22 | spec=$($GET "$COCKPIT_GIT/main/tools/cockpit.spec") 23 | OS_VER_NO_VARIANT="suse_version $(rpm --eval '%suse_version')" 24 | ;; 25 | *) 26 | spec=$($GET "$COCKPIT_GIT/main/tools/cockpit.spec") 27 | ;; 28 | esac 29 | 30 | echo "$spec" | rpmspec -D "$OS_VER_NO_VARIANT" -D 'version 0' -D 'enable_old_bridge 0' --buildrequires --query /dev/stdin | sed 's/.*/"&"/' | tr '\n' ' ' 31 | 32 | # some extra build dependencies: 33 | # - libappstream-glib for validating appstream metadata in starter-kit and derivatives 34 | # - rpmlint for validating built RPMs 35 | # - gettext to build/merge GNU gettext translations 36 | # - desktop-file-utils for validating desktop files 37 | # - nodejs for starter-kit and other projects which rebuild webpack during RPM build 38 | case "$OS_VER" in 39 | *suse*) 40 | EXTRA_DEPS="appstream-glib rpmlint gettext-runtime desktop-file-utils nodejs-default" 41 | ;; 42 | rhel*10|centos*10) 43 | # no rpmlint in RHEL 10: https://pkgs.devel.redhat.com/cgit/rpms/rpmlint/commit/?h=rhel-10-main&id=9a9efcbfd844 44 | EXTRA_DEPS="libappstream-glib gettext desktop-file-utils nodejs" 45 | ;; 46 | *) 47 | EXTRA_DEPS="libappstream-glib rpmlint gettext desktop-file-utils nodejs" 48 | ;; 49 | esac 50 | 51 | # TEMP: cockpit needs python3-devel to select the default Python version 52 | EXTRA_DEPS="$EXTRA_DEPS python3-devel" 53 | 54 | # libappstream-glib-devel is needed for merging translations in AppStream XML files in starter-kit and derivatives 55 | # on RHEL 8 only: gettext in RHEL 8 does not know about .metainfo.xml files, and libappstream-glib-devel 56 | # provides /usr/share/gettext/its/appdata.{its,loc} for them 57 | case "$OS_VER" in 58 | rhel*8|centos*8) EXTRA_DEPS="$EXTRA_DEPS libappstream-glib-devel" ;; 59 | *) ;; 60 | esac 61 | 62 | # pull nodejs-devel on Fedora for compliance with the guidelines on using nodejs modules: 63 | # https://docs.fedoraproject.org/en-US/packaging-guidelines/Node.js/#_buildrequires 64 | case "$OS_VER" in 65 | fedora*eln) ;; 66 | fedora*) EXTRA_DEPS="$EXTRA_DEPS nodejs-devel" ;; 67 | *) ;; 68 | esac 69 | 70 | echo "$EXTRA_DEPS" 71 | -------------------------------------------------------------------------------- /images/scripts/lib/cloudimage.bootstrap: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | set -ex 4 | 5 | out=$1 6 | image_url="$2" 7 | size=${3:-+8G} 8 | 9 | # download cloud image; re-use a previously downloaded image 10 | image=tmp/$(basename $image_url) 11 | mkdir -p $(dirname $image) 12 | [ -f "$image" ] || curl -L -o "$image" "$image_url" 13 | cp "$image" "$out" 14 | qemu-img resize -f qcow2 "$out" "$size" 15 | -------------------------------------------------------------------------------- /images/scripts/lib/cockpit-ci.fcc: -------------------------------------------------------------------------------- 1 | # Documentation: https://coreos.github.io/butane/ 2 | # Download compiler from https://github.com/coreos/butane/releases 3 | # Build with butane-x86_64-unknown-linux-gnu --pretty --output images/scripts/lib/cockpit-ci.ign images/scripts/lib/cockpit-ci.fcc 4 | variant: fcos 5 | version: 1.1.0 6 | passwd: 7 | users: 8 | - name: admin 9 | gecos: Administrator 10 | # foobar 11 | password_hash: $6$mNBJSioSQeVZR.Hp$B7T3O2owkci1XYj5CDOUNotUKNAT7mNDHRi8lqdoThFt7YZQKDmbmX7INado6YniSbyA1YJx0lWbT3GHsoAaJ0 12 | ssh_authorized_keys: 13 | # from machine/identity.pub 14 | - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDUOtNJdBEXyKxBB898rdT54ULjMGuO6v4jLXmRsdRhR5Id/lKNc9hsdioPWUePgYlqML2iSV72vKQoVhkyYkpcsjr3zvBny9+5xej3+TBLoEMAm2hmllKPmxYJDU8jQJ7wJuRrOVOnk0iSNF+FcY/yaQ0owSF02Nphx47j2KWc0IjGGlt4fl0fmHJuZBA2afN/4IYIIsEWZziDewVtaEjWV3InMRLllfdqGMllhFR+ed2hQz9PN2QcapmEvUR4UCy/mJXrke5htyFyHi8ECfyMMyYeHwbWLFQIve4CWix9qtksvKjcetnxT+WWrutdr3c9cfIj/c0v/Zg/c4zETxtp cockpit-test 15 | groups: ["wheel"] 16 | # same data for root 17 | - name: root 18 | password_hash: $6$mNBJSioSQeVZR.Hp$B7T3O2owkci1XYj5CDOUNotUKNAT7mNDHRi8lqdoThFt7YZQKDmbmX7INado6YniSbyA1YJx0lWbT3GHsoAaJ0 19 | ssh_authorized_keys: 20 | - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDUOtNJdBEXyKxBB898rdT54ULjMGuO6v4jLXmRsdRhR5Id/lKNc9hsdioPWUePgYlqML2iSV72vKQoVhkyYkpcsjr3zvBny9+5xej3+TBLoEMAm2hmllKPmxYJDU8jQJ7wJuRrOVOnk0iSNF+FcY/yaQ0owSF02Nphx47j2KWc0IjGGlt4fl0fmHJuZBA2afN/4IYIIsEWZziDewVtaEjWV3InMRLllfdqGMllhFR+ed2hQz9PN2QcapmEvUR4UCy/mJXrke5htyFyHi8ECfyMMyYeHwbWLFQIve4CWix9qtksvKjcetnxT+WWrutdr3c9cfIj/c0v/Zg/c4zETxtp cockpit-test 21 | storage: 22 | files: 23 | # enable ssh password authentication 24 | # https://docs.fedoraproject.org/en-US/fedora-coreos/authentication/#_enabling_ssh_password_authentication 25 | - path: /etc/ssh/sshd_config.d/02-enable-passwords.conf 26 | mode: 0644 27 | contents: 28 | inline: | 29 | # Fedora CoreOS disables SSH password login by default. 30 | # Enable it. 31 | # This file must sort before 04-disable-passwords.conf. 32 | PasswordAuthentication yes 33 | # don't hang/fail on non-existing DHCP on ens15 (second interface for mcast) on boot 34 | # similar to images/scripts/network-ifcfg-eth1 for Fedora/RHEL images 35 | - path: /etc/NetworkManager/system-connections/mcast1.nmconnection 36 | mode: 0600 37 | contents: 38 | inline: | 39 | [connection] 40 | id=mcast1 41 | type=ethernet 42 | interface-name=ens15 43 | autoconnect-priority=-100 44 | 45 | [ipv4] 46 | method=disabled 47 | 48 | [ipv6] 49 | method=ignore 50 | systemd: 51 | units: 52 | # this is a really saddening way of turning off the VM after setup 53 | - name: poweroff.service 54 | enabled: true 55 | contents: | 56 | [Unit] 57 | Description=Power off machine after boot 58 | After=multi-user.target 59 | 60 | [Service] 61 | Type=oneshot 62 | ExecStart=/bin/systemctl disable %n 63 | ExecStart=/bin/rm /etc/systemd/system/%n 64 | ExecStart=/sbin/shutdown --poweroff now 65 | 66 | [Install] 67 | WantedBy=multi-user.target 68 | -------------------------------------------------------------------------------- /images/scripts/lib/cockpit-ci.ign: -------------------------------------------------------------------------------- 1 | { 2 | "ignition": { 3 | "version": "3.1.0" 4 | }, 5 | "passwd": { 6 | "users": [ 7 | { 8 | "gecos": "Administrator", 9 | "groups": [ 10 | "wheel" 11 | ], 12 | "name": "admin", 13 | "passwordHash": "$6$mNBJSioSQeVZR.Hp$B7T3O2owkci1XYj5CDOUNotUKNAT7mNDHRi8lqdoThFt7YZQKDmbmX7INado6YniSbyA1YJx0lWbT3GHsoAaJ0", 14 | "sshAuthorizedKeys": [ 15 | "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDUOtNJdBEXyKxBB898rdT54ULjMGuO6v4jLXmRsdRhR5Id/lKNc9hsdioPWUePgYlqML2iSV72vKQoVhkyYkpcsjr3zvBny9+5xej3+TBLoEMAm2hmllKPmxYJDU8jQJ7wJuRrOVOnk0iSNF+FcY/yaQ0owSF02Nphx47j2KWc0IjGGlt4fl0fmHJuZBA2afN/4IYIIsEWZziDewVtaEjWV3InMRLllfdqGMllhFR+ed2hQz9PN2QcapmEvUR4UCy/mJXrke5htyFyHi8ECfyMMyYeHwbWLFQIve4CWix9qtksvKjcetnxT+WWrutdr3c9cfIj/c0v/Zg/c4zETxtp cockpit-test" 16 | ] 17 | }, 18 | { 19 | "name": "root", 20 | "passwordHash": "$6$mNBJSioSQeVZR.Hp$B7T3O2owkci1XYj5CDOUNotUKNAT7mNDHRi8lqdoThFt7YZQKDmbmX7INado6YniSbyA1YJx0lWbT3GHsoAaJ0", 21 | "sshAuthorizedKeys": [ 22 | "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDUOtNJdBEXyKxBB898rdT54ULjMGuO6v4jLXmRsdRhR5Id/lKNc9hsdioPWUePgYlqML2iSV72vKQoVhkyYkpcsjr3zvBny9+5xej3+TBLoEMAm2hmllKPmxYJDU8jQJ7wJuRrOVOnk0iSNF+FcY/yaQ0owSF02Nphx47j2KWc0IjGGlt4fl0fmHJuZBA2afN/4IYIIsEWZziDewVtaEjWV3InMRLllfdqGMllhFR+ed2hQz9PN2QcapmEvUR4UCy/mJXrke5htyFyHi8ECfyMMyYeHwbWLFQIve4CWix9qtksvKjcetnxT+WWrutdr3c9cfIj/c0v/Zg/c4zETxtp cockpit-test" 23 | ] 24 | } 25 | ] 26 | }, 27 | "storage": { 28 | "files": [ 29 | { 30 | "path": "/etc/ssh/sshd_config.d/02-enable-passwords.conf", 31 | "contents": { 32 | "source": "data:,%23%20Fedora%20CoreOS%20disables%20SSH%20password%20login%20by%20default.%0A%23%20Enable%20it.%0A%23%20This%20file%20must%20sort%20before%2004-disable-passwords.conf.%0APasswordAuthentication%20yes%0A" 33 | }, 34 | "mode": 420 35 | }, 36 | { 37 | "path": "/etc/NetworkManager/system-connections/mcast1.nmconnection", 38 | "contents": { 39 | "source": "data:,%5Bconnection%5D%0Aid%3Dmcast1%0Atype%3Dethernet%0Ainterface-name%3Dens15%0Aautoconnect-priority%3D-100%0A%0A%5Bipv4%5D%0Amethod%3Ddisabled%0A%0A%5Bipv6%5D%0Amethod%3Dignore%0A" 40 | }, 41 | "mode": 384 42 | } 43 | ] 44 | }, 45 | "systemd": { 46 | "units": [ 47 | { 48 | "contents": "[Unit]\nDescription=Power off machine after boot\nAfter=multi-user.target\n\n[Service]\nType=oneshot\nExecStart=/bin/systemctl disable %n\nExecStart=/bin/rm /etc/systemd/system/%n\nExecStart=/sbin/shutdown --poweroff now\n\n[Install]\nWantedBy=multi-user.target\n", 49 | "enabled": true, 50 | "name": "poweroff.service" 51 | } 52 | ] 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /images/scripts/lib/make-srpm: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eu 4 | 5 | tar=$1 6 | 7 | version=$(echo "$1" | sed -n 's|.*cockpit-\([^ /-]\+\)\.tar\..*|\1|p') 8 | if [ -z "$version" ]; then 9 | echo "make-srpm: couldn't parse version from tarball: $1" 10 | exit 2 11 | fi 12 | 13 | # We actually modify the spec so that the srpm is standalone buildable 14 | modify_spec() { 15 | sed -e "/^Version:.*/d" -e "1i\ 16 | %define wip wip\nVersion: $version\n" 17 | } 18 | 19 | tmpdir=$(mktemp -d $PWD/srpm-build.XXXXXX) 20 | tar xaf "$1" -O cockpit-$version/tools/cockpit.spec | modify_spec > $tmpdir/cockpit.spec 21 | 22 | rpmbuild -bs \ 23 | --quiet \ 24 | --define "_sourcedir $(dirname $1)" \ 25 | --define "_specdir $tmpdir" \ 26 | --define "_builddir $tmpdir" \ 27 | --define "_srcrpmdir `pwd`" \ 28 | --define "_rpmdir $tmpdir" \ 29 | --define "_buildrootdir $tmpdir/.build" \ 30 | $tmpdir/cockpit.spec 31 | 32 | rpm --qf '%{Name}-%{Version}-%{Release}.src.rpm\n' -q --specfile $tmpdir/cockpit.spec | head -n1 33 | rm -rf $tmpdir 34 | -------------------------------------------------------------------------------- /images/scripts/lib/mcast1.nmconnection: -------------------------------------------------------------------------------- 1 | # don't hang/fail on non-existing DHCP on ens15 (second interface for mcast) on boot 2 | 3 | [connection] 4 | id=mcast1 5 | type=ethernet 6 | interface-name=ens15 7 | autoconnect-priority=-100 8 | 9 | [ipv4] 10 | method=disabled 11 | 12 | [ipv6] 13 | method=ignore 14 | -------------------------------------------------------------------------------- /images/scripts/lib/podman-images.setup: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eux 3 | 4 | # these are available for many architectures, and supported/updated reasonably well 5 | podman pull quay.io/prometheus/busybox 6 | podman pull quay.io/jitesoft/alpine 7 | podman pull quay.io/libpod/registry:2.8 8 | 9 | # podman tests expect the images with a neutral name, so re-tag them 10 | podman tag quay.io/prometheus/busybox localhost/test-busybox 11 | podman rmi quay.io/prometheus/busybox 12 | podman tag quay.io/jitesoft/alpine localhost/test-alpine 13 | podman rmi quay.io/jitesoft/alpine 14 | podman tag quay.io/libpod/registry:2.8 localhost/test-registry 15 | podman rmi quay.io/libpod/registry:2.8 16 | 17 | if [ "$(podman -v | awk '{ print substr($3, 1, 1) }')" -lt 4 ]; then 18 | podman pull docker://k8s.gcr.io/pause:3.5 19 | fi 20 | -------------------------------------------------------------------------------- /images/scripts/lib/pubring.gpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cockpit-project/bots/c44c83d5ba464fc66439988e484b7fa875d46472/images/scripts/lib/pubring.gpg -------------------------------------------------------------------------------- /images/scripts/lib/secring.gpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cockpit-project/bots/c44c83d5ba464fc66439988e484b7fa875d46472/images/scripts/lib/secring.gpg -------------------------------------------------------------------------------- /images/scripts/lib/zero-disk.setup: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This file is part of Cockpit. 4 | # 5 | # Copyright (C) 2016 Red Hat, Inc. 6 | # 7 | # Cockpit is free software; you can redistribute it and/or modify it 8 | # under the terms of the GNU Lesser General Public License as published by 9 | # the Free Software Foundation; either version 2.1 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # Cockpit is distributed in the hope that it will be useful, but 13 | # WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 15 | # Lesser General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Lesser General Public License 18 | # along with Cockpit; If not, see . 19 | 20 | # We don't want to delete the pbuilder caches since we need them during build. 21 | # Mock with --offline and dnf is sometimes happy without caches, and with yum it 22 | # never is, so we provide an option to also leave the mock caches in place. 23 | # 24 | # We also want to keep cracklib since otherwise password quality 25 | # checks break on Debian. 26 | 27 | if [ -f /root/.skip-zero-disk ]; then 28 | echo "Skipping zero-disk.setup as /root/.skip-zero-disk exists" 29 | exit 0 30 | fi 31 | 32 | keep="! -path /var/cache/pbuilder ! -path /var/cache/pacman ! -path /var/cache/cracklib ! -path /var/cache/tomcat" 33 | while [ $# -gt 0 ]; do 34 | case $1 in 35 | --keep-mock-cache) 36 | keep="$keep ! -path /var/cache/mock" 37 | ;; 38 | esac 39 | shift 40 | done 41 | 42 | if [ -d "/var/cache" ]; then 43 | find /var/cache/* -maxdepth 0 -depth -name "*" $keep -exec rm -rf {} \; 44 | fi 45 | rm -rf /var/tmp/* 46 | # try to not continue to write to /var for this setup boot 47 | # HACK: hangs forever in c8s due to https://bugzilla.redhat.com/show_bug.cgi?id=2174645 48 | timeout 10 journalctl --relinquish-var || true 49 | rm -rf /var/log/journal/* 50 | 51 | if [ $(findmnt / -n -o fstype) != "btrfs" ]; then 52 | dd if=/dev/zero of=/root/junk || true 53 | sync 54 | rm -f /root/junk 55 | fi 56 | -------------------------------------------------------------------------------- /images/scripts/opensuse-tumbleweed.bootstrap: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -eux 3 | 4 | URL='https://download.opensuse.org/tumbleweed/appliances/' 5 | IMAGE="openSUSE-Tumbleweed-Minimal-VM.x86_64-Cloud.qcow2" 6 | 7 | exec $(dirname $0)/lib/cloudimage.bootstrap "$1" "$URL/$IMAGE" 8 | -------------------------------------------------------------------------------- /images/scripts/ostree.setup: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -eux 3 | 4 | IMAGE="$1" 5 | 6 | podman pull quay.io/cockpit/ws 7 | # HACK: latest is broken: https://gitlab.com/jitesoft/dockerfiles/nginx/-/issues/2 8 | podman pull quay.io/jitesoft/nginx:stable 9 | # the tests don't specify a tag, so re-tag stable 10 | podman tag quay.io/jitesoft/nginx:stable quay.io/jitesoft/nginx:latest 11 | 12 | # for c-podman tests 13 | /var/lib/testvm/podman-images.setup 14 | 15 | # Prevent SSH from hanging for a long time when no external network access 16 | echo 'UseDNS no' >> /etc/ssh/sshd_config.d/10-no-usedns.conf 17 | 18 | # disable ens15 to avoid long boot hang on NetworkManager-wait-online.service 19 | nmcli con add con-name "ens15" ifname ens15 type ethernet ipv4.method disabled ipv6.method ignore 20 | 21 | if [ "$IMAGE" == "fedora-coreos" ]; then 22 | # disable automatic updates 23 | systemctl disable --now zincati.service 24 | 25 | # pre-install the distro version, which is useful for testing extensions and manual experiments 26 | # also install glib-networking, so that tests can install cockpit-ws (as long as it has that dependency) 27 | rpm-ostree install cockpit-system cockpit-bridge cockpit-networkmanager glib-networking 28 | fi 29 | 30 | # Wait for all systemd jobs to finish before cleaning up. In 31 | # particular, this will allow kdump.service to finish creating the 32 | # kdump initramfs. 33 | 34 | while [ -n "$(systemctl list-jobs --legend=no)" ]; do 35 | sleep 10 36 | done 37 | 38 | # Installing RPMs in a OSTree image sometimes triggers something that 39 | # will change the mtime of all of /etc on the next boot. This in turn 40 | # will trigger a regeneration of the kdump initrd. This would happen 41 | # in each test run, which is wasteful and also might interfere with 42 | # the test itself. Let's just switch the automatic regeneration off. 43 | 44 | ! test -e /etc/kdump.conf || echo "force_no_rebuild 1" >>/etc/kdump.conf 45 | 46 | # Disable PerSourcePenalties, they interfere with the rapid failed 47 | # logins performed by some tests. 48 | echo "PerSourcePenalties no" >/etc/ssh/sshd_config.d/99-no-penalties.conf 49 | 50 | # reduce image size 51 | rpm-ostree cleanup --repomd 52 | /var/lib/testvm/zero-disk.setup 53 | -------------------------------------------------------------------------------- /images/scripts/rhel-10-0.bootstrap: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ex 3 | 4 | URL=http://download.devel.redhat.com/rhel-10/nightly/RHEL-10/latest-RHEL-10.0/compose/BaseOS/x86_64/images/ 5 | IMAGE=$(curl -L -s "$URL" | sed -n '/ "$CACHE" 13 | xz -cd "$CACHE" > "$OUTPUT" 14 | 15 | # boot it once to run ignition 16 | qemu-system-x86_64 -enable-kvm -nographic -m 1024 -device virtio-rng-pci \ 17 | -drive file="$OUTPUT",if=virtio -fw_cfg name=opt/com.coreos/config,file=$BASE/lib/cockpit-ci.ign 18 | -------------------------------------------------------------------------------- /images/scripts/ubuntu-2204.bootstrap: -------------------------------------------------------------------------------- 1 | #! /bin/sh -ex 2 | exec $(dirname $0)/lib/cloudimage.bootstrap "$1" https://cloud-images.ubuntu.com/daily/server/jammy/current/jammy-server-cloudimg-amd64.img 3 | -------------------------------------------------------------------------------- /images/scripts/ubuntu-2204.setup: -------------------------------------------------------------------------------- 1 | debian.setup -------------------------------------------------------------------------------- /images/scripts/ubuntu-2404.bootstrap: -------------------------------------------------------------------------------- 1 | #! /bin/sh -ex 2 | 3 | exec $(dirname $0)/lib/cloudimage.bootstrap "$1" "https://cloud-images.ubuntu.com/daily/server/noble/current/noble-server-cloudimg-amd64.img" 4 | -------------------------------------------------------------------------------- /images/scripts/ubuntu-2404.setup: -------------------------------------------------------------------------------- 1 | debian.setup -------------------------------------------------------------------------------- /images/scripts/ubuntu-stable.bootstrap: -------------------------------------------------------------------------------- 1 | #! /bin/sh -ex 2 | 3 | # determine latest stable release (see https://launchpad.net/+apidoc) 4 | # in most cases the current series is devel, except for right after a stable release 5 | rel=$(curl --silent https://api.launchpad.net/devel/ubuntu/current_series_link | sed 's/^"//; s/"$//') 6 | if ! curl --silent "$rel" | grep -q '"supported": true'; then 7 | # not supported, go back 8 | rel=$(curl --silent "$rel/previous_series_link" | sed 's/^"//; s/"$//') 9 | 10 | if ! curl --silent "$rel" | grep -q '"supported": true'; then 11 | echo "ERROR: neither of the last two releases are supported!?" >&2 12 | exit 1 13 | fi 14 | fi 15 | # release name is the last part of the URL 16 | rel=${rel##*/} 17 | 18 | exec $(dirname $0)/lib/cloudimage.bootstrap "$1" "https://cloud-images.ubuntu.com/daily/server/$rel/current/$rel-server-cloudimg-amd64.img" 19 | -------------------------------------------------------------------------------- /images/scripts/ubuntu-stable.setup: -------------------------------------------------------------------------------- 1 | debian.setup -------------------------------------------------------------------------------- /images/services: -------------------------------------------------------------------------------- 1 | services-9b512d0e8ed6f5678ea3662e8569676963c908007a8f90552efedec63254c1aa.qcow2 -------------------------------------------------------------------------------- /images/ubuntu-2204: -------------------------------------------------------------------------------- 1 | ubuntu-2204-acd5049cffb795e8906ac185231fbb7275b2280a6b669a28aab9e6b81b8ec6eb.qcow2 -------------------------------------------------------------------------------- /images/ubuntu-2404: -------------------------------------------------------------------------------- 1 | ubuntu-2404-5990451c28a3c2e41b9672fc486e3e71f2d14591e9ff3592673d94332b12902b.qcow2 -------------------------------------------------------------------------------- /images/ubuntu-stable: -------------------------------------------------------------------------------- 1 | ubuntu-stable-a5aeaadf03941705ee1de95e2b7547bdcde62f52380aff687231a0ad4e377a62.qcow2 -------------------------------------------------------------------------------- /inspect-queue: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # This file is part of Cockpit. 4 | # 5 | # Copyright (C) 2018 Red Hat, Inc. 6 | # 7 | # Cockpit is free software; you can redistribute it and/or modify it 8 | # under the terms of the GNU Lesser General Public License as published by 9 | # the Free Software Foundation; either version 2.1 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # Cockpit is distributed in the hope that it will be useful, but 13 | # WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 15 | # Lesser General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Lesser General Public License 18 | # along with Cockpit; If not, see . 19 | 20 | import argparse 21 | import json 22 | import sys 23 | 24 | from task import distributed_queue 25 | 26 | MAX_PRIORITY = 9 27 | 28 | 29 | def main() -> int: 30 | parser = argparse.ArgumentParser(description='Read and print messages from the queue without acknowleding them') 31 | parser.add_argument('--amqp', default=distributed_queue.DEFAULT_AMQP_SERVER, 32 | help='The host:port of the AMQP server to consume from (default: %(default)s)') 33 | parser.add_argument('--human', action='store_true', help="Print the 'human' field, if present") 34 | parser.add_argument('--secrets-dir', default=distributed_queue.DEFAULT_SECRETS_DIR, 35 | help='Directory with ca.pem and amqp-client.{pem,key} (default: %(default)s)') 36 | opts = parser.parse_args() 37 | 38 | with distributed_queue.DistributedQueue(opts.amqp, ['public', 'rhel', 'statistics', 'webhook'], 39 | secrets_dir=opts.secrets_dir, passive=True) as q: 40 | def print_queue(queue: str) -> None: 41 | message_count = q.queue_counts.get(queue, 0) 42 | if message_count == 0: 43 | print(f"queue {queue} does not exist or is empty") 44 | return 45 | for _ in range(message_count): 46 | method_frame, _header_frame, message = q.channel.basic_get(queue=queue) 47 | if method_frame and message: 48 | body = json.loads(message) 49 | if opts.human and 'human' in body: 50 | print(body['human']) 51 | else: 52 | print(body) 53 | 54 | print('public queue:') 55 | print_queue('public') 56 | print('rhel queue:') 57 | print_queue('rhel') 58 | print('statistics queue:') 59 | print_queue('statistics') 60 | print('webhook queue:') 61 | print_queue('webhook') 62 | 63 | return 0 64 | 65 | 66 | if __name__ == '__main__': 67 | sys.exit(main()) 68 | -------------------------------------------------------------------------------- /issues-review: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | import time 5 | 6 | from task import github 7 | 8 | bug_msg = """ 9 | This issue is closed due to inactivity. 10 | 11 | If you are still able to reproduce it, please comment here and provide us with 12 | an updated reproducer. 13 | """ 14 | 15 | rfe_msg = """ 16 | This issue is closed due to inactivity. 17 | 18 | We are sorry, but we won't be able to address this in any time soon. 19 | If you are interested in helping us with the implementation, please comment here. 20 | """ 21 | 22 | general_msg = """ 23 | This issue is closed due to inactivity. 24 | 25 | If this issue is a bug report and you are still able to reproduce it, please 26 | comment here and provide us with an updated reproducer. 27 | 28 | If this issue is a request for a new feature, we are sorry, but we won't be able 29 | to address this in any time soon. If you are interested in helping us with 30 | the implementation, please comment here. 31 | """ 32 | 33 | 34 | def issues_review(api: github.GitHub, opts: argparse.Namespace) -> None: 35 | now = time.time() 36 | treshold = opts.age * 86400 37 | count = 100 38 | page = 1 39 | while count == 100: 40 | issues = api.get("issues?filter=all&page=%i&per_page=%i" % (page, count)) 41 | page += 1 42 | count = len(issues) 43 | for issue in issues: 44 | age = now - time.mktime(time.strptime(issue["updated_at"], "%Y-%m-%dT%H:%M:%SZ")) 45 | if age >= treshold: 46 | print("Labelling #%i last updated at %s" % (issue["number"], issue["updated_at"])) 47 | api.post("issues/%i/labels" % issue["number"], [opts.label]) 48 | 49 | 50 | def close_issues(api: github.GitHub, opts: argparse.Namespace) -> None: 51 | count = 100 52 | page = 1 53 | while count == 100: 54 | issues = api.get("issues?filter=all&page=%i&per_page=%i&labels=%s" % (page, count, opts.label)) 55 | page += 1 56 | count = len(issues) 57 | for issue in issues: 58 | labels = api.get("issues/%i/labels" % issue["number"]) 59 | enhancement = [la for la in labels if la["name"] == "enhancement"] 60 | bug = [la for la in labels if la["name"] == "bug"] 61 | if bug: 62 | print("Closing #%i as stale bug" % issue["number"]) 63 | api.post("issues/%i/comments" % issue["number"], {"body": bug_msg}) 64 | elif enhancement: 65 | print("Closing #%i as stale enhancement" % issue["number"]) 66 | api.post("issues/%i/comments" % issue["number"], {"body": rfe_msg}) 67 | else: 68 | print("Closing #%i as stale bug or enhancement" % issue["number"]) 69 | api.post("issues/%i/comments" % issue["number"], {"body": general_msg}) 70 | api.post("issues/%i" % issue["number"], {"state": "closed"}) 71 | 72 | 73 | def main() -> None: 74 | parser = argparse.ArgumentParser(description='Label or close labeled stable issues') 75 | parser.add_argument('-a', '--age', metavar='DAYS', default=90, 76 | help='Label issues whose last update is older than DAYS (default: %(default)s)') 77 | parser.add_argument('-l', '--label', default=time.strftime('review-%Y-%m'), 78 | help='Label name (default: %(default)s)') 79 | parser.add_argument('-c', '--close', action='store_true', 80 | help='Close all issues with the label') 81 | parser.add_argument('--repo', help='Work on this GitHub repository (owner/name)') 82 | opts = parser.parse_args() 83 | 84 | api = github.GitHub(repo=opts.repo) 85 | if opts.close: 86 | close_issues(api, opts) 87 | else: 88 | issues_review(api, opts) 89 | 90 | 91 | if __name__ == '__main__': 92 | main() 93 | -------------------------------------------------------------------------------- /job-runner: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright (C) 2024 Red Hat, Inc. 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU Affero General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU Affero General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Affero General Public License 16 | # along with this program. If not, see . 17 | 18 | import argparse 19 | import asyncio 20 | import logging 21 | import sys 22 | from pathlib import Path 23 | from typing import assert_never 24 | 25 | from lib.aio.job import Job, run_job 26 | from lib.aio.jobcontext import JobContext 27 | from lib.aio.jsonutil import JsonError 28 | from lib.aio.util import JsonObjectAction, KeyValueAction 29 | 30 | logger = logging.getLogger(__name__) 31 | 32 | BOTS_DIR = Path(__file__).parent 33 | 34 | 35 | async def main() -> None: 36 | parser = argparse.ArgumentParser() 37 | parser.add_argument('--debug', action='store_true', help="Enable debugging output") 38 | parser.add_argument('--config-file', '-F', metavar='FILENAME', 39 | help="Config file [default JOB_RUNNER_CONFIG or ~/.config/cockpit-dev/job-runner.toml]") 40 | subprograms = parser.add_subparsers(dest='cmd', required=True, title="Subcommands") 41 | 42 | run_parser = subprograms.add_parser("run", help="Run a single job provided on the command line") 43 | run_parser.add_argument('repo', help="The repository (like `cockpit-project/cockpit`)") 44 | run_parser.add_argument('--pull', type=int, help="The pull request number to run tests on") 45 | run_parser.add_argument('--sha', help="The revision sha, exactly 40 hex digits") 46 | run_parser.add_argument('--context', help="The status we're reporting against the sha") 47 | run_parser.add_argument('--target', help="The target branch") 48 | run_parser.add_argument('--report', action=JsonObjectAction, help="Open an issue on failures") 49 | run_parser.add_argument('--slug', help="The URL slug (used for logging)") 50 | run_parser.add_argument('--title', help="The title for the log page") 51 | run_parser.add_argument('--container', help="The container image (like `ghcr.io/cockpit-project/tasks:latest`)") 52 | run_parser.add_argument('--env', help="Environment variables for the test run", action=KeyValueAction) 53 | run_parser.add_argument('--timeout', type=int, help="Timeout of the job, in minutes", default=120) 54 | run_parser.add_argument('--secret', dest='secrets', default=[], action='append', help="Provide the named secret") 55 | run_parser.add_argument('command', nargs='*', help="Command to run [default: .cockpit-ci/run]") 56 | 57 | run_parser = subprograms.add_parser("json", help="Run a single given as a JSON blob") 58 | run_parser.add_argument('json', action=JsonObjectAction, help="The job, in JSON format") 59 | 60 | args = parser.parse_args() 61 | 62 | if args.debug: 63 | logging.basicConfig(level=logging.DEBUG) 64 | else: 65 | logging.basicConfig(level=logging.INFO) 66 | 67 | match args.cmd: 68 | case 'run': 69 | # if this throws, it's an error in the parser setup, above 70 | job = Job(vars(args)) 71 | 72 | case 'json': 73 | try: 74 | job = Job(args.json) 75 | except JsonError as exc: 76 | sys.exit(f'Poorly formed job: {exc}') 77 | 78 | case other: 79 | assert_never(other) 80 | 81 | async with JobContext(args.config_file, debug=args.debug) as ctx: 82 | await run_job(job, ctx) 83 | 84 | 85 | if __name__ == '__main__': 86 | asyncio.run(main()) 87 | -------------------------------------------------------------------------------- /job-runner.toml: -------------------------------------------------------------------------------- 1 | # Default configuration for job-runner 2 | 3 | # This file contains the default settings for job-runner and will always be 4 | # read in order to get the defaults. It is also meant to act as a rudimentary 5 | # form of documentation for the options which are available. 6 | 7 | # Anything in this file can be overridden by installing a file with a similar 8 | # format in one of the following locations (in order of precedence): 9 | # - the path specified via `--config-file` 10 | # - the path specified via the `JOB_RUNNER_CONFIG` environment variable 11 | # - XDG_CONFIG_HOME/cockpit-dev/job-runner.toml 12 | 13 | # The defaults from this file are merged with overrides from the first such 14 | # file found. 15 | 16 | # The default configuration is intentionally broken. You'll need to provide a 17 | # configuration which (at least) does one of: 18 | # - provides a valid GitHub API token; and/or 19 | # - sets forge.github.post to false 20 | 21 | [container] 22 | command = ['podman'] 23 | run-args = [ 24 | # '--device=/dev/kvm' 25 | ] 26 | default-image = 'ghcr.io/cockpit-project/tasks:latest' 27 | 28 | [container.secrets] 29 | # see podman-secret(1) 30 | # github-token = ['--secret=github-token'] 31 | 32 | [logs] 33 | driver='local' # 's3' or 'local' 34 | 35 | [logs.s3] 36 | # hint: podman run --rm --net=host quay.io/minio/minio server /var 37 | url = 'http://127.0.0.1:9000/tmp/' 38 | key = {access='minioadmin', secret='minioadmin'} 39 | user-agent = 'job-runner (cockpit-project/bots)' 40 | acl = 'public-read' 41 | 42 | [logs.local] 43 | # hint: python -m http.server -b 127.0.0.1 -d ~/.cache/cockpit-dev/job-runner-logs 44 | dir = '~/.cache/cockpit-dev/job-runner-logs' 45 | link = 'http://127.0.0.1:8000/' 46 | 47 | [forge] 48 | driver='github' 49 | 50 | [forge.github] 51 | clone-url = 'https://github.com/' 52 | api-url = 'https://api.github.com/' 53 | post = true # whether to post statuses, open issues, etc. 54 | user-agent = 'job-runner (cockpit-project/bots)' 55 | # (at least) one of `token` or `post = false` must be set 56 | # token = 'ghp_XXX' 57 | -------------------------------------------------------------------------------- /lib/__init__.py: -------------------------------------------------------------------------------- 1 | from .allowlist import ALLOWLIST 2 | 3 | __all__ = [ 4 | 'ALLOWLIST', 5 | ] 6 | -------------------------------------------------------------------------------- /lib/aio/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cockpit-project/bots/c44c83d5ba464fc66439988e484b7fa875d46472/lib/aio/__init__.py -------------------------------------------------------------------------------- /lib/aio/base.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2024 Red Hat, Inc. 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU Affero General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU Affero General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU Affero General Public License 14 | # along with this program. If not, see . 15 | 16 | from collections.abc import Collection 17 | from typing import AsyncContextManager, NamedTuple, Self 18 | 19 | from yarl import URL 20 | 21 | from .jsonutil import JsonObject, get_int, get_str 22 | 23 | 24 | class SubjectSpecification: 25 | def __init__(self, obj: JsonObject) -> None: 26 | self.repo = get_str(obj, 'repo') 27 | self.sha = get_str(obj, 'sha', None) 28 | self.pull = get_int(obj, 'pull', None) 29 | self.branch = get_str(obj, 'branch', None) 30 | self.target = get_str(obj, 'target', None) 31 | 32 | 33 | class Subject(NamedTuple): 34 | forge: 'Forge' 35 | repo: str 36 | sha: str 37 | rebase: str | None = None 38 | 39 | @property 40 | def clone_url(self) -> URL: 41 | return self.forge.clone / self.repo 42 | 43 | @property 44 | def content_url(self) -> URL: 45 | return self.forge.content / self.repo 46 | 47 | 48 | class Status: 49 | link: str 50 | 51 | async def post(self, state: str, description: str) -> None: 52 | raise NotImplementedError 53 | 54 | 55 | class Forge: 56 | clone: URL 57 | content: URL 58 | 59 | async def resolve_subject(self, spec: SubjectSpecification) -> Subject: 60 | raise NotImplementedError 61 | 62 | async def check_pr_changed(self, repo: str, pull_nr: int, expected_sha: str) -> str | None: 63 | raise NotImplementedError 64 | 65 | def get_status(self, repo: str, sha: str, context: str | None, location: URL) -> Status: 66 | raise NotImplementedError 67 | 68 | async def open_issue(self, repo: str, issue: JsonObject) -> None: 69 | raise NotImplementedError 70 | 71 | async def read_file(self, subject: Subject, filename: str) -> str | None: 72 | raise NotImplementedError 73 | 74 | @classmethod 75 | def new(cls, config: JsonObject) -> Self: 76 | raise NotImplementedError 77 | 78 | 79 | class Destination: 80 | location: URL 81 | 82 | def has(self, filename: str) -> bool: 83 | raise NotImplementedError 84 | 85 | def write(self, filename: str, data: bytes) -> None: 86 | raise NotImplementedError 87 | 88 | def delete(self, filenames: Collection[str]) -> None: 89 | raise NotImplementedError 90 | 91 | 92 | class LogDriver: 93 | def get_destination(self, slug: str) -> AsyncContextManager[Destination]: 94 | raise NotImplementedError 95 | 96 | @classmethod 97 | def new(cls, config: JsonObject) -> Self: 98 | raise NotImplementedError 99 | -------------------------------------------------------------------------------- /lib/aio/local.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2024 Red Hat, Inc. 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU Affero General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU Affero General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU Affero General Public License 14 | # along with this program. If not, see . 15 | 16 | import contextlib 17 | import logging 18 | import os 19 | from collections.abc import AsyncIterator, Collection 20 | from pathlib import Path 21 | 22 | from yarl import URL 23 | 24 | from .base import Destination, LogDriver 25 | from .jsonutil import JsonObject, get_str 26 | 27 | logger = logging.getLogger(__name__) 28 | 29 | 30 | class LocalDestination(Destination): 31 | def __init__(self, directory: Path, location: URL) -> None: 32 | logger.debug('LocalDestination(%r, %r)', directory, location) 33 | self.dir = directory 34 | self.location = location 35 | os.makedirs(self.dir, exist_ok=True) 36 | 37 | def has(self, filename: str) -> bool: 38 | return (self.dir / filename).exists() 39 | 40 | def write(self, filename: str, data: bytes) -> None: 41 | logger.debug('Write %s', self.dir / filename) 42 | (self.dir / filename).write_bytes(data) 43 | 44 | def delete(self, filenames: Collection[str]) -> None: 45 | for filename in filenames: 46 | logger.debug('Delete %s', self.dir / filename) 47 | (self.dir / filename).unlink() 48 | 49 | 50 | class LocalLogDriver(LogDriver, contextlib.AsyncExitStack): 51 | def __init__(self, config: JsonObject) -> None: 52 | super().__init__() 53 | self.directory = Path(get_str(config, 'dir')).expanduser() 54 | self.link = URL(get_str(config, 'link')) 55 | 56 | @contextlib.asynccontextmanager 57 | async def get_destination(self, slug: str) -> AsyncIterator[Destination]: 58 | yield LocalDestination(self.directory / slug, self.link / slug) 59 | -------------------------------------------------------------------------------- /lib/aio/spawn.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2024 Red Hat, Inc. 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU Affero General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU Affero General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU Affero General Public License 14 | # along with this program. If not, see . 15 | 16 | import asyncio 17 | import contextlib 18 | import logging 19 | from collections.abc import AsyncIterator, Sequence 20 | from typing import Any 21 | 22 | logger = logging.getLogger(__name__) 23 | 24 | 25 | @contextlib.asynccontextmanager 26 | async def spawn(args: Sequence[str], **kwargs: Any) -> AsyncIterator[asyncio.subprocess.Process]: 27 | logger.debug('spawn(%r)', args) 28 | process = await asyncio.create_subprocess_exec(*args, **kwargs) 29 | pid = process.pid 30 | logger.debug('spawn: pid %r', pid) 31 | try: 32 | yield process 33 | finally: 34 | logger.debug('spawn: waiting for pid %r', pid) 35 | status = await process.wait() 36 | logger.debug('spawn: pid %r exited, %r', pid, status) 37 | 38 | 39 | async def run(args: Sequence[str], **kwargs: Any) -> None: 40 | logger.debug('run(%r)', args) 41 | process = await asyncio.create_subprocess_exec(*args, **kwargs) 42 | pid = process.pid 43 | logger.debug('run: waiting for pid %r', pid) 44 | status = await process.wait() 45 | logger.debug('run: pid %r exited, %r', pid, status) 46 | -------------------------------------------------------------------------------- /lib/allowlist.py: -------------------------------------------------------------------------------- 1 | # bots and organizations which are allowed to use our CI 2 | 3 | ALLOWLIST = { 4 | # orgs 5 | 'candlepin', 6 | 'cockpit-project', 7 | 'osbuild', 8 | 'rhinstaller', 9 | 10 | # bots 11 | 'cockpituous', 12 | 'github-actions[bot]', 13 | 14 | # humans 15 | 16 | # cockpit team + contributors 17 | 'ashley-cui', 18 | 'allisonkarlitskaya', 19 | 'garrett', 20 | 'jelly', 21 | 'jscotka', 22 | 'martinpitt', 23 | 'marusak', 24 | 'mvollmer', 25 | 'subhoghoshX', 26 | 'tomasmatus', 27 | 'Venefilyn', 28 | 'yunmingyang', 29 | 30 | # installer team + contributors 31 | 'KKoukiou', 32 | 'M4rtinK', 33 | 'adamkankovsky', 34 | 'elkoniu', 35 | 'jkonecny12', 36 | 'pkratoch', 37 | 'rvykydal', 38 | 'vojtechtrefny', 39 | 40 | # osbuild team + contributors 41 | 'croissanne', 42 | 'jkozol', 43 | 'lucasgarfield', 44 | 'regexowl', 45 | 'supakeen', 46 | 47 | # candlepin team + contributors 48 | 'ptoscano', 49 | 'pkoprda', 50 | } 51 | -------------------------------------------------------------------------------- /lib/constants.py: -------------------------------------------------------------------------------- 1 | # This file is part of Cockpit. 2 | # 3 | # Copyright (C) 2013 Red Hat, Inc. 4 | # 5 | # Cockpit is free software; you can redistribute it and/or modify it 6 | # under the terms of the GNU Lesser General Public License as published by 7 | # the Free Software Foundation; either version 2.1 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # Cockpit is distributed in the hope that it will be useful, but 11 | # WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | # Lesser General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Lesser General Public License 16 | # along with Cockpit; If not, see . 17 | 18 | import os 19 | 20 | # Images which are OSTree based 21 | OSTREE_IMAGES = ("centos-9-bootc", "fedora-coreos") 22 | 23 | LIB_DIR = os.path.dirname(__file__) 24 | BOTS_DIR = os.path.dirname(LIB_DIR) 25 | MACHINE_DIR = os.path.join(BOTS_DIR, 'machine') 26 | 27 | # bots always act on the project that is the current directory 28 | # FIXME: Get rid of these aliases and drop their usage everywhere, once that approach works 29 | BASE_DIR = os.getcwd() 30 | TEST_DIR = os.path.join(BASE_DIR, "test") 31 | GIT_DIR = os.path.join(BASE_DIR, ".git") 32 | 33 | IMAGES_DIR = os.path.join(BOTS_DIR, "images") 34 | SCRIPTS_DIR = os.path.join(IMAGES_DIR, "scripts") 35 | 36 | DEFAULT_IDENTITY_FILE = os.path.join(MACHINE_DIR, "identity") 37 | DEFAULT_IDENTITY_PUB_FILE = os.path.join(MACHINE_DIR, "identity.pub") 38 | 39 | TEST_OS_DEFAULT = "fedora-42" 40 | DEFAULT_IMAGE = os.environ.get("TEST_OS", TEST_OS_DEFAULT) 41 | -------------------------------------------------------------------------------- /lib/directories.py: -------------------------------------------------------------------------------- 1 | # This file is part of Cockpit. 2 | # 3 | # Copyright (C) 2019 Red Hat, Inc. 4 | # 5 | # Cockpit is free software; you can redistribute it and/or modify it 6 | # under the terms of the GNU Lesser General Public License as published by 7 | # the Free Software Foundation; either version 2.1 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # Cockpit is distributed in the hope that it will be useful, but 11 | # WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | # Lesser General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Lesser General Public License 16 | # along with Cockpit; If not, see . 17 | 18 | import functools 19 | import os 20 | import subprocess 21 | 22 | from .constants import GIT_DIR 23 | 24 | 25 | def xdg_home(subdir: str, envvar: str, *components: str, override: str | None = None) -> str: 26 | path = override and os.getenv(override) 27 | 28 | if not path: 29 | directory = os.getenv(envvar) 30 | if not directory: 31 | directory = os.path.join(os.path.expanduser('~'), subdir) 32 | path = os.path.join(directory, *components) 33 | 34 | return path 35 | 36 | 37 | def xdg_config_home(*components: str, envvar: str | None = None) -> str: 38 | return xdg_home('.config', 'XDG_CONFIG_HOME', *components, override=envvar) 39 | 40 | 41 | def xdg_cache_home(*components: str, envvar: str | None = None) -> str: 42 | return xdg_home('.cache', 'XDG_CACHE_HOME', *components, override=envvar) 43 | 44 | 45 | def get_git_config(*args: str) -> str | None: 46 | if not os.path.exists(GIT_DIR): 47 | return None 48 | 49 | try: 50 | myenv = os.environ.copy() 51 | myenv["GIT_DIR"] = GIT_DIR 52 | return subprocess.check_output(('git', 'config', *args), text=True, env=myenv).strip() 53 | 54 | except (OSError, subprocess.CalledProcessError): # 'git' not in PATH, or cmd fails 55 | return None 56 | 57 | 58 | @functools.cache 59 | def get_images_data_dir() -> str: 60 | images_data_dir = os.getenv('COCKPIT_IMAGES_DATA_DIR') 61 | 62 | if images_data_dir is None: 63 | images_data_dir = get_git_config('--type=path', 'cockpit.bots.images-data-dir') 64 | 65 | if images_data_dir is None: 66 | images_data_dir = xdg_cache_home('cockpit-images') 67 | 68 | return images_data_dir 69 | -------------------------------------------------------------------------------- /lib/jobqueue.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2024 Red Hat, Inc. 2 | # 3 | # This program is free software: you can redistribute it and/or modify 4 | # it under the terms of the GNU Affero General Public License as published by 5 | # the Free Software Foundation, either version 3 of the License, or 6 | # (at your option) any later version. 7 | # 8 | # This program is distributed in the hope that it will be useful, 9 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | # GNU Affero General Public License for more details. 12 | # 13 | # You should have received a copy of the GNU Affero General Public License 14 | # along with this program. If not, see . 15 | 16 | from collections.abc import Mapping, Sequence 17 | from typing import Required, TypedDict 18 | 19 | from lib.aio.jsonutil import JsonObject 20 | 21 | 22 | class SubjectSpecification(TypedDict, total=False): 23 | repo: Required[str] 24 | sha: str | None 25 | branch: str | None 26 | pull: int | None 27 | 28 | 29 | class JobSpecification(SubjectSpecification, total=False): 30 | context: Required[str] 31 | slug: str 32 | report: JsonObject | None 33 | command_subject: SubjectSpecification | None 34 | command: Sequence[str] 35 | env: Mapping[str, str] 36 | secrets: Sequence[str] 37 | 38 | 39 | class QueueEntry(TypedDict): 40 | job: JobSpecification 41 | human: str 42 | -------------------------------------------------------------------------------- /lib/network.py: -------------------------------------------------------------------------------- 1 | # This file is part of Cockpit. 2 | # 3 | # Copyright (C) 2022 Red Hat, Inc. 4 | # 5 | # Cockpit is free software; you can redistribute it and/or modify it 6 | # under the terms of the GNU Lesser General Public License as published by 7 | # the Free Software Foundation; either version 2.1 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # Cockpit is distributed in the hope that it will be useful, but 11 | # WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | # Lesser General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Lesser General Public License 16 | # along with Cockpit; If not, see . 17 | 18 | # Shared GitHub code. When run as a script, we print out info about 19 | # our GitHub interacition. 20 | 21 | import functools 22 | import os 23 | import socket 24 | import ssl 25 | 26 | from lib.constants import IMAGES_DIR 27 | 28 | # Cockpit image/log server CA 29 | CA_PEM = os.getenv("COCKPIT_CA_PEM", os.path.join(IMAGES_DIR, "files", "ca.pem")) 30 | 31 | 32 | CA_PEM_DOMAINS = [ 33 | "e2e.bos.redhat.com", 34 | # development/cockpituous project tests 35 | "localdomain", 36 | ] 37 | 38 | 39 | def get_host_ca(hostname: str) -> str | None: 40 | """Return custom CA that applies to the given host name. 41 | 42 | Self-hosted infrastructure uses CA_PEM, while publicly hosted infrastructure ought to have 43 | an officially trusted TLS certificate. Return None for these. 44 | """ 45 | # strip off port 46 | hostname = hostname.split(':')[0] 47 | 48 | if any((hostname == domain or hostname.endswith("." + domain)) for domain in CA_PEM_DOMAINS): 49 | return CA_PEM 50 | return None 51 | 52 | 53 | def get_curl_ca_arg(hostname: str) -> list[str]: 54 | """Return curl CLI arguments for talking to hostname. 55 | 56 | This uses get_host_ca() to determine an appropriate CA for talking to hostname. 57 | Returns ["--cacert", "CAFilePath"] or [] as approprioate. 58 | """ 59 | ca = get_host_ca(hostname) 60 | return ['--cacert', ca] if ca else [] 61 | 62 | 63 | def host_ssl_context(hostname: str) -> ssl.SSLContext | None: 64 | """Return SSLContext suitable for given hostname. 65 | 66 | This uses get_host_ca() to determine an appropriate CA. 67 | """ 68 | cafile = get_host_ca(hostname) 69 | return ssl.create_default_context(cafile=cafile) if cafile else None 70 | 71 | 72 | @functools.lru_cache 73 | def redhat_network() -> bool: 74 | """Check if we can access the Red Hat network 75 | 76 | The result gets cached, so this can be called several times. 77 | """ 78 | try: 79 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 80 | s.settimeout(10) 81 | s.connect(("download.devel.redhat.com", 443)) 82 | return True 83 | except OSError: 84 | return False 85 | -------------------------------------------------------------------------------- /lib/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cockpit-project/bots/c44c83d5ba464fc66439988e484b7fa875d46472/lib/py.typed -------------------------------------------------------------------------------- /lib/stores.py: -------------------------------------------------------------------------------- 1 | # This file is part of Cockpit. 2 | # 3 | # Copyright (C) 2022 Red Hat, Inc. 4 | # 5 | # Cockpit is free software; you can redistribute it and/or modify it 6 | # under the terms of the GNU Lesser General Public License as published by 7 | # the Free Software Foundation; either version 2.1 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # Cockpit is distributed in the hope that it will be useful, but 11 | # WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | # Lesser General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Lesser General Public License 16 | # along with Cockpit; If not, see . 17 | 18 | from collections.abc import Sequence 19 | 20 | from lib.directories import xdg_config_home 21 | 22 | # hosted on public internet 23 | PUBLIC_STORES: Sequence[str] = ( 24 | "https://cockpit-images.eu-central-1.linodeobjects.com/", 25 | "https://cockpit-images.us-east-1.linodeobjects.com/", 26 | ) 27 | 28 | # hosted behind the Red Hat VPN 29 | REDHAT_STORES: Sequence[str] = ( 30 | # e2e down for maintenance 31 | # "https://cockpit-11.apps.cnfdb2.e2e.bos.redhat.com/images/", 32 | ) 33 | 34 | # local stores 35 | try: 36 | with open(xdg_config_home('cockpit-dev', 'image-stores', envvar='COCKPIT_IMAGE_STORES_FILE')) as fp: 37 | data = fp.read().strip() 38 | if data: 39 | PUBLIC_STORES = (*PUBLIC_STORES, *data.split("\n")) 40 | except FileNotFoundError: 41 | # that config file is optional 42 | pass 43 | 44 | 45 | LOG_STORE = "https://cockpit-logs.us-east-1.linodeobjects.com/" 46 | -------------------------------------------------------------------------------- /machine/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cockpit-project/bots/c44c83d5ba464fc66439988e484b7fa875d46472/machine/__init__.py -------------------------------------------------------------------------------- /machine/cloud-init.iso: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cockpit-project/bots/c44c83d5ba464fc66439988e484b7fa875d46472/machine/cloud-init.iso -------------------------------------------------------------------------------- /machine/host_key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEAr+bCynyw7hAG03Bwt3joCTPjrexdO+ynsA+HtZRs38N9NCaO 3 | MZ7j7KCRFUgkezo7GEAp7lRparZWrzAixcyATZNOokwYP55flvsWtwhTSE2wI4gY 4 | n+0nmNFy+l3qs29VWFzVX8CkCqXBiGw53uo8qLuMEWVdXmstNxR00pHdvlyjOhjl 5 | BpZBFKD8gMGDx6qClGIosgcSbNtJf6Xl1ceo7BoLNknOoJdyiT9EwdhO53A9aVhx 6 | kbYjbIRRVWq8P2Cq/kbPioYlUtwgAH2A4aQTVlzsEyssdnriYwIbERddG8eqZ7mn 7 | UhKU/FH6Of2BSFQA9Rh6bVC0s1Y1KCZupLaBwQIDAQABAoIBAQCbjHLA4NcNDjsb 8 | CxmCBXcbfDlged5QuYvoEzOtDN3iWlsDnPytQJbJj4v8x9kK54mOfl8WFKtL5IZv 9 | UR/OznK/Jv6oYqYmzAQ33T5PCRusmpaiNR2hfvQ/HSiR4i9EEbXk9+LwU8g8aivk 10 | WeArEfQmOgM49uxELH7FcF+GPdtbE9TsHNdkVf1CzCMcGdIeNjeCqEDQrgRSdAq8 11 | YpCrlvQj76Gv8g6IOUiMZYS/fqbuvMR/XryXSQkEUX/4I5QojZOD1XrzxGA94jJ9 12 | dOv3Yr1y2+fhPAy5dDIFoqWSuDMib2yGV47jFo+Mqu6ovLVPAt74UWucHKImXgo1 13 | qvl0wFkJAoGBANwmWqJZ8dxJTU5gcK2KOq3u2JUYSYek3HMkEsPjEezGtht1Okg5 14 | TjxFEiw+yc4yeUtj0lIOyNU976FA0+5ItiW18/Byw6zWUi2BmrLGsCM0/CL/xwKM 15 | hVo8DrMXcGrZY6ZSqNiLtAYLmgAUKkJEP+pw8r1Qr0pO5yfVHNeK0v/3AoGBAMyL 16 | xWIhETGKkmyCuqFSFPELxbmMwjqWagNrzFK23/cqgbFv0aCz6wXhcwQ5JiszFq7B 17 | Hvz8Wezl9Ur2FdFz3wGz46q+Cdqnw7uQTTGd5WbDWHN/tCS67bKn998BqENpPiWK 18 | OIgNFXnNcucFtte9o7QDmjSaDd4u0xwveRYwHg4HAoGBAJWPbOV864X3OpCzjfkn 19 | vmOprvPjUxjW1HlYmXMA0Y2lFdSjmFu2qsLhPc5XPaxat/KStzDOIHxWHnTTYOcx 20 | +KS37yh8HxlNZPjLYrhvqPvSJDT2xVGi+3lo8aeTlejRFRTKdTDgAAZXXWEOUgNA 21 | 8Jcp8o7QwLVf00RJUNXR1zTTAoGAChy+3WMVHoXjR0oPP/p23pPeapXy5EKbax/h 22 | MhWobOfFEaidjHxYminTLdpFcM1NycXyaj9vkq6rudEAsyIvXD4wezh59D1nB9bS 23 | eil8NeBidxNRLJ+xMKvtLTE/yFVjpSd4NAGxlhv6GkHGEFRny3aCISecl+douHQA 24 | YIBwe/ECgYALzEEkESm8d5Zq2fuFtUhRqFGcOtr/IYR8OgtUIZe2NRImsR+r5ycN 25 | w4mw7RAnxKqOoXeAtWBwi5MykItiaof2MD3MIe4kxlZQt0NPpyE4dkzsUkYf89kE 26 | ndu5mUalV7s7KBttm9gn8e+btzERna2VPRfDQh8nHw/zLXtE7lFSUg== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /machine/host_key.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCv5sLKfLDuEAbTcHC3eOgJM+Ot7F077KewD4e1lGzfw300Jo4xnuPsoJEVSCR7OjsYQCnuVGlqtlavMCLFzIBNk06iTBg/nl+W+xa3CFNITbAjiBif7SeY0XL6Xeqzb1VYXNVfwKQKpcGIbDne6jyou4wRZV1eay03FHTSkd2+XKM6GOUGlkEUoPyAwYPHqoKUYiiyBxJs20l/peXVx6jsGgs2Sc6gl3KJP0TB2E7ncD1pWHGRtiNshFFVarw/YKr+Rs+KhiVS3CAAfYDhpBNWXOwTKyx2euJjAhsRF10bx6pnuadSEpT8Ufo5/YFIVAD1GHptULSzVjUoJm6ktoHB 2 | -------------------------------------------------------------------------------- /machine/identity: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpQIBAAKCAQEA1DrTSXQRF8isQQfPfK3U+eFC4zBrjur+Iy15kbHUYUeSHf5S 3 | jXPYbHYqD1lHj4GJajC9okle9rykKFYZMmJKXLI6987wZ8vfucXo9/kwS6BDAJto 4 | ZpZSj5sWCQ1PI0Ce8CbkazlTp5NIkjRfhXGP8mkNKMEhdNjaYceO49ilnNCIxhpb 5 | eH5dH5hybmQQNmnzf+CGCCLBFmc4g3sFbWhI1ldyJzES5ZX3ahjJZYRUfnndoUM/ 6 | TzdkHGqZhL1EeFAsv5iV65HuYbchch4vBAn8jDMmHh8G1ixUCL3uAlosfarZLLyo 7 | 3HrZ8U/llq7rXa93PXHyI/3NL/2YP3OMxE8baQIDAQABAoIBAQCxuOUwkKqzsQ9W 8 | kdTWArfj3RhnKigYEX9qM+2m7TT9lbKtvUiiPc2R3k4QdmIvsXlCXLigyzJkCsqp 9 | IJiPEbJV98bbuAan1Rlv92TFK36fBgC15G5D4kQXD/ce828/BSFT2C3WALamEPdn 10 | v8Xx+Ixjokcrxrdeoy4VTcjB0q21J4C2wKP1wEPeMJnuTcySiWQBdAECCbeZ4Vsj 11 | cmRdcvL6z8fedRPtDW7oec+IPkYoyXPktVt8WsQPYkwEVN4hZVBneJPCcuhikYkp 12 | T3WGmPV0MxhUvCZ6hSG8D2mscZXRq3itXVlKJsUWfIHaAIgGomWrPuqC23rOYCdT 13 | 5oSZmTvFAoGBAPs1FbbxDDd1fx1hisfXHFasV/sycT6ggP/eUXpBYCqVdxPQvqcA 14 | ktplm5j04dnaQJdHZ8TPlwtL+xlWhmhFhlCFPtVpU1HzIBkp6DkSmmu0gvA/i07Z 15 | pzo5Z+HRZFzruTQx6NjDtvWwiXVLwmZn2oiLeM9xSqPu55OpITifEWNjAoGBANhH 16 | XwV6IvnbUWojs7uiSGsXuJOdB1YCJ+UF6xu8CqdbimaVakemVO02+cgbE6jzpUpo 17 | krbDKOle4fIbUYHPeyB0NMidpDxTAPCGmiJz7BCS1fCxkzRgC+TICjmk5zpaD2md 18 | HCrtzIeHNVpTE26BAjOIbo4QqOHBXk/WPen1iC3DAoGBALsD3DSj46puCMJA2ebI 19 | 2EoWaDGUbgZny2GxiwrvHL7XIx1XbHg7zxhUSLBorrNW7nsxJ6m3ugUo/bjxV4LN 20 | L59Gc27ByMvbqmvRbRcAKIJCkrB1Pirnkr2f+xx8nLEotGqNNYIawlzKnqr6SbGf 21 | Y2wAGWKmPyEoPLMLWLYkhfdtAoGANsFa/Tf+wuMTqZuAVXCwhOxsfnKy+MNy9jiZ 22 | XVwuFlDGqVIKpjkmJyhT9KVmRM/qePwgqMSgBvVOnszrxcGRmpXRBzlh6yPYiQyK 23 | 2U4f5dJG97j9W7U1TaaXcCCfqdZDMKnmB7hMn8NLbqK5uLBQrltMIgt1tjIOfofv 24 | BNx0raECgYEApAvjwDJ75otKz/mvL3rUf/SNpieODBOLHFQqJmF+4hrSOniHC5jf 25 | f5GS5IuYtBQ1gudBYlSs9fX6T39d2avPsZjfvvSbULXi3OlzWD8sbTtvQPuCaZGI 26 | Df9PUWMYZ3HRwwdsYovSOkT53fG6guy+vElUEDkrpZYczROZ6GUcx70= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /machine/identity.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDUOtNJdBEXyKxBB898rdT54ULjMGuO6v4jLXmRsdRhR5Id/lKNc9hsdioPWUePgYlqML2iSV72vKQoVhkyYkpcsjr3zvBny9+5xej3+TBLoEMAm2hmllKPmxYJDU8jQJ7wJuRrOVOnk0iSNF+FcY/yaQ0owSF02Nphx47j2KWc0IjGGlt4fl0fmHJuZBA2afN/4IYIIsEWZziDewVtaEjWV3InMRLllfdqGMllhFR+ed2hQz9PN2QcapmEvUR4UCy/mJXrke5htyFyHi8ECfyMMyYeHwbWLFQIve4CWix9qtksvKjcetnxT+WWrutdr3c9cfIj/c0v/Zg/c4zETxtp cockpit-test 2 | -------------------------------------------------------------------------------- /machine/machine_core/__init__.py: -------------------------------------------------------------------------------- 1 | # Place holder for python module 2 | -------------------------------------------------------------------------------- /machine/machine_core/cli.py: -------------------------------------------------------------------------------- 1 | # This file is part of Cockpit. 2 | # 3 | # Copyright (C) 2013 Red Hat, Inc. 4 | # 5 | # Cockpit is free software; you can redistribute it and/or modify it 6 | # under the terms of the GNU Lesser General Public License as published by 7 | # the Free Software Foundation; either version 2.1 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # Cockpit is distributed in the hope that it will be useful, but 11 | # WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | # Lesser General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Lesser General Public License 16 | # along with Cockpit; If not, see . 17 | 18 | import argparse 19 | import signal 20 | 21 | from . import machine_virtual 22 | 23 | 24 | def cmd_cli() -> None: 25 | parser = argparse.ArgumentParser(description="Run a VM image until SIGTERM or SIGINT") 26 | parser.add_argument("--memory", type=int, default=machine_virtual.MEMORY_MB, 27 | help="Memory in MiB to allocate to the VM (default: %(default)s)") 28 | parser.add_argument("-v", "--verbose", action="store_true", help="Enable verbose logging") 29 | parser.add_argument("image", help="Image name") 30 | args = parser.parse_args() 31 | 32 | network = machine_virtual.VirtNetwork(0, image=args.image) 33 | machine = machine_virtual.VirtMachine(image=args.image, networking=network.host(), memory_mb=args.memory, 34 | verbose=args.verbose) 35 | machine.start() 36 | machine.wait_boot() 37 | 38 | # run a command to force starting the SSH master 39 | machine.execute('uptime') 40 | 41 | # print ssh command 42 | print("ssh -o ControlPath=%s -p %s %s@%s" % 43 | (machine.ssh_master, machine.ssh_port, machine.ssh_user, machine.ssh_address)) 44 | # print Cockpit web address 45 | print(f"http://{machine.web_address}:{machine.web_port}") 46 | # print marker that the VM is ready; tests can poll for this to wait for the VM 47 | print("RUNNING") 48 | 49 | signal.signal(signal.SIGTERM, lambda sig, frame: machine.stop()) 50 | try: 51 | signal.pause() 52 | except KeyboardInterrupt: 53 | machine.stop() 54 | -------------------------------------------------------------------------------- /machine/machine_core/exceptions.py: -------------------------------------------------------------------------------- 1 | # This file is part of Cockpit. 2 | # 3 | # Copyright (C) 2013 Red Hat, Inc. 4 | # 5 | # Cockpit is free software; you can redistribute it and/or modify it 6 | # under the terms of the GNU Lesser General Public License as published by 7 | # the Free Software Foundation; either version 2.1 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # Cockpit is distributed in the hope that it will be useful, but 11 | # WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | # Lesser General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Lesser General Public License 16 | # along with Cockpit; If not, see . 17 | 18 | 19 | class Failure(Exception): 20 | def __init__(self, msg: str) -> None: 21 | self.msg = msg 22 | 23 | def __str__(self) -> str: 24 | return self.msg 25 | -------------------------------------------------------------------------------- /machine/machine_core/testvm.py: -------------------------------------------------------------------------------- 1 | # This file is part of Cockpit. 2 | # 3 | # Copyright (C) 2013 Red Hat, Inc. 4 | # 5 | # Cockpit is free software; you can redistribute it and/or modify it 6 | # under the terms of the GNU Lesser General Public License as published by 7 | # the Free Software Foundation; either version 2.1 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # Cockpit is distributed in the hope that it will be useful, but 11 | # WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | # Lesser General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Lesser General Public License 16 | # along with Cockpit; If not, see . 17 | 18 | from lib.constants import BOTS_DIR, DEFAULT_IMAGE, TEST_DIR, TEST_OS_DEFAULT 19 | 20 | from .exceptions import Failure 21 | from .machine import Machine 22 | from .machine_virtual import VirtMachine, VirtNetwork 23 | from .timeout import Timeout 24 | 25 | __all__ = ( 26 | "BOTS_DIR", 27 | "DEFAULT_IMAGE", 28 | "TEST_DIR", 29 | "TEST_OS_DEFAULT", 30 | "Failure", 31 | "Machine", 32 | "Timeout", 33 | "VirtMachine", 34 | "VirtNetwork" 35 | ) 36 | -------------------------------------------------------------------------------- /machine/machine_core/timeout.py: -------------------------------------------------------------------------------- 1 | # This file is part of Cockpit. 2 | # 3 | # Copyright (C) 2013 Red Hat, Inc. 4 | # 5 | # Cockpit is free software; you can redistribute it and/or modify it 6 | # under the terms of the GNU Lesser General Public License as published by 7 | # the Free Software Foundation; either version 2.1 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # Cockpit is distributed in the hope that it will be useful, but 11 | # WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | # Lesser General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Lesser General Public License 16 | # along with Cockpit; If not, see . 17 | 18 | import signal 19 | import typing 20 | 21 | if typing.TYPE_CHECKING: 22 | from .ssh_connection import SSHConnection 23 | 24 | 25 | class Timeout: 26 | """ Add a timeout to an operation 27 | Specify machine to ensure that a machine's ssh operations are canceled when the timer expires. 28 | """ 29 | def __init__(self, seconds: int = 1, error_message: str = 'Timeout', machine: 'SSHConnection | None' = None): 30 | if signal.getsignal(signal.SIGALRM) != signal.SIG_DFL: 31 | # there is already a different Timeout active 32 | self.seconds = None 33 | return 34 | 35 | self.seconds = seconds 36 | self.error_message = error_message 37 | self.machine = machine 38 | 39 | def handle_timeout(self, _signum: int, _frame: object) -> None: 40 | if self.machine: 41 | if self.machine.ssh_process: 42 | self.machine.ssh_process.terminate() 43 | self.machine.disconnect() 44 | 45 | raise RuntimeError(self.error_message) 46 | 47 | def __enter__(self) -> None: 48 | if self.seconds: 49 | signal.signal(signal.SIGALRM, self.handle_timeout) 50 | signal.alarm(self.seconds) 51 | 52 | def __exit__(self, _type: object, _value: object, _traceback: object) -> None: 53 | if self.seconds: 54 | signal.alarm(0) 55 | signal.signal(signal.SIGALRM, signal.SIG_DFL) 56 | -------------------------------------------------------------------------------- /machine/make-cloud-init-iso: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | # This file is part of Cockpit. 4 | # 5 | # Copyright (C) 2015 Red Hat, Inc. 6 | # 7 | # Cockpit is free software; you can redistribute it and/or modify it 8 | # under the terms of the GNU Lesser General Public License as published by 9 | # the Free Software Foundation; either version 2.1 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # Cockpit is distributed in the hope that it will be useful, but 13 | # WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 15 | # Lesser General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Lesser General Public License 18 | # along with Cockpit; If not, see . 19 | 20 | set -e 21 | 22 | # create the cloud-init iso 23 | init_dir=$(mktemp -d) 24 | meta_data="$init_dir/meta-data" 25 | user_data="$init_dir/user-data" 26 | iso_image="cloud-init.iso" 27 | 28 | # $ mkpasswd --method=sha256crypt --salt=CockpitCloudInit foobar 29 | vm_pass='$5$CockpitCloudInit$Iw89f.aPgqHPXAHC2Zs9h9335n3E1FQDFvR6MLqwPK9' 30 | key_pub=`cat identity.pub` 31 | host_key=`sed 's/^/ /' host_key` 32 | host_key_pub=`cat host_key.pub` 33 | 34 | mkdir -p $init_dir 35 | 36 | # We don't want to hardcode values: 37 | # local-hostname: we want multiple instances of the vm to run in parallel 38 | # instance-id: cloud-init skips some init stuff if this is constant (e.g. runcmd) 39 | cat >$meta_data <$user_data < /etc/ssh/sshd_config"] 74 | 75 | # make sure that our user script runs on every boot 76 | cloud_final_modules: 77 | - scripts-per-once 78 | - scripts-per-boot 79 | - scripts-per-instance 80 | - [scripts-user, always] 81 | - final-message 82 | 83 | EOF 84 | 85 | genisoimage -input-charset utf-8 -output $iso_image -volid cidata -joliet -rock $user_data $meta_data 86 | -------------------------------------------------------------------------------- /machine/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cockpit-project/bots/c44c83d5ba464fc66439988e484b7fa875d46472/machine/py.typed -------------------------------------------------------------------------------- /machine/testvm.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 -u 2 | 3 | # This file is part of Cockpit. 4 | # 5 | # Copyright (C) 2013 Red Hat, Inc. 6 | # 7 | # Cockpit is free software; you can redistribute it and/or modify it 8 | # under the terms of the GNU Lesser General Public License as published by 9 | # the Free Software Foundation; either version 2.1 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # Cockpit is distributed in the hope that it will be useful, but 13 | # WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 15 | # Lesser General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Lesser General Public License 18 | # along with Cockpit; If not, see . 19 | 20 | import os 21 | import sys 22 | 23 | if os.path.realpath(f'{__file__}/../..') not in sys.path: 24 | # ensure that the top-level is present in the path so the following imports work 25 | sys.path.insert(1, os.path.realpath(f'{__file__}/../..')) 26 | 27 | from lib.constants import BOTS_DIR, DEFAULT_IMAGE, IMAGES_DIR, SCRIPTS_DIR, TEST_DIR, TEST_OS_DEFAULT 28 | from lib.directories import get_images_data_dir 29 | from lib.testmap import get_build_image, get_test_image 30 | from machine.machine_core.cli import cmd_cli 31 | from machine.machine_core.exceptions import Failure 32 | from machine.machine_core.machine import Machine 33 | from machine.machine_core.machine_virtual import VirtMachine, VirtNetwork 34 | from machine.machine_core.timeout import Timeout 35 | 36 | __all__ = ( 37 | "BOTS_DIR", 38 | "DEFAULT_IMAGE", 39 | "IMAGES_DIR", 40 | "SCRIPTS_DIR", 41 | "TEST_DIR", 42 | "TEST_OS_DEFAULT", 43 | "Failure", 44 | "Machine", 45 | "Timeout", 46 | "VirtMachine", 47 | "VirtNetwork", 48 | "get_build_image", 49 | "get_images_data_dir", 50 | "get_test_image" 51 | ) 52 | 53 | # This can be used as helper program for tests not written in Python: Run given 54 | # image name until SIGTERM or SIGINT; the image must exist in test/images/; 55 | # use image-prepare or image-customize to create that. For example: 56 | # $ bots/image-customize -v -i cockpit debian-stable 57 | # $ bots/machine/testvm.py debian-stable 58 | if __name__ == "__main__": 59 | cmd_cli() 60 | -------------------------------------------------------------------------------- /naughty-prune: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # This file is part of Cockpit. 4 | # 5 | # Copyright (C) 2017 Red Hat, Inc. 6 | # 7 | # Cockpit is free software; you can redistribute it and/or modify it 8 | # under the terms of the GNU Lesser General Public License as published by 9 | # the Free Software Foundation; either version 2.1 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # Cockpit is distributed in the hope that it will be useful, but 13 | # WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 15 | # Lesser General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Lesser General Public License 18 | # along with Cockpit; If not, see . 19 | 20 | # To use this example add a line to an issue with the "bot" label 21 | # 22 | # * [ ] naughty-prune 23 | # 24 | 25 | import subprocess 26 | import sys 27 | import time 28 | 29 | import task 30 | from lib.constants import BASE_DIR 31 | 32 | sys.dont_write_bytecode = True 33 | 34 | # Number of days after which a known issue is treated as stale 35 | DAYS = 21 36 | 37 | # In case this ever changes 38 | SECONDS_PER_DAY = 3600 * 24 39 | 40 | 41 | def execute(*args): 42 | if task.verbose: 43 | sys.stderr.write("+ " + " ".join(args) + "\n") 44 | return subprocess.check_output(args, cwd=BASE_DIR, text=True) 45 | 46 | 47 | def run(unused, verbose=False, **kwargs): 48 | 49 | # Since a month ago 50 | now = time.time() 51 | since = now - (DAYS * SECONDS_PER_DAY) 52 | 53 | # The head 54 | head = execute("git", "rev-parse", "HEAD").strip() 55 | 56 | # Get all the open known issues 57 | issues = task.api.issues(labels=["knownissue"]) 58 | for issue in issues: 59 | number = issue["number"] 60 | 61 | updated = issue.get("updated_at", None) 62 | if not updated: 63 | updated = issue.get("created_at", None) 64 | if not updated: 65 | continue 66 | 67 | # Don't touch recently updated issues 68 | updated = time.mktime(time.strptime(updated, "%Y-%m-%dT%H:%M:%SZ")) 69 | if updated > since: 70 | continue 71 | 72 | # Try to close this issue 73 | execute("git", "checkout", "--detach", head) 74 | execute("find", "naughty/", "-name", f"{number}-*", "-delete") 75 | 76 | # Create a pull request from these changes 77 | title = f"naughty: Close {number}: {issue['title']}" 78 | days = int((now - updated) / SECONDS_PER_DAY) 79 | body = f"Known issue which has not occurred in {days} days\n\n{issue['title']}\n\nFixes #{number}" 80 | branch = task.branch(number, f"{title}\n\n{body}", pathspec="naughty/", **kwargs) 81 | 82 | if branch: 83 | kwargs["title"] = title 84 | pull = task.pull(branch, body=body, **kwargs) 85 | task.comment(pull, "Please comment on the upstream distro bug manually before accepting this " 86 | "pull request.\n\nIf you wish to keep this known issue open, then update it") 87 | 88 | 89 | if __name__ == '__main__': 90 | task.main(function=run, title="Close known issues") 91 | -------------------------------------------------------------------------------- /naughty/arch/4796-stratis-runs-clevis-too-early: -------------------------------------------------------------------------------- 1 | File "check-storage-stratis", line *, in testBasic 2 | b.wait_text(self.card_row_col("Stratis filesystems", 1, 1), "fsys1") # should be started after boot 3 | -------------------------------------------------------------------------------- /naughty/arch/5090-lvm2-resize-ntfs-unexpected-error: -------------------------------------------------------------------------------- 1 | Traceback (most recent call last): 2 | File "*", line *, in testResizeNtfs 3 | self.checkResize("ntfs", crypto=False, 4 | * 5 | testlib.Error: timeout 6 | -------------------------------------------------------------------------------- /naughty/arch/5090-lvresize-fails-with-stratis-signature: -------------------------------------------------------------------------------- 1 | > warn*: Error resizing logical volume: Process reported exit code 5: File system device usage is not available from libblkid. 2 | * 3 | File "check-storage-stratis", line *, in testPoolResize 4 | -------------------------------------------------------------------------------- /naughty/arch/7646-libvirt-attach-disk-segfault: -------------------------------------------------------------------------------- 1 | Traceback (most recent call last): 2 | File "test/machineslib.py", line *, in tearDown 3 | super().tearDown() 4 | * 5 | File "test/common/testlib.py", line *, in tearDown 6 | self.check_journal_messages() 7 | * 8 | File "test/common/testlib.py", line *, in check_journal_messages 9 | raise Error(UNEXPECTED_MESSAGE + "journal messages:\n" + first) 10 | testlib.Error: FAIL: Test completed, but found unexpected journal messages: 11 | Process * (libvirtd) of user * terminated abnormally with signal 11/SEGV, processing... 12 | -------------------------------------------------------------------------------- /naughty/arch/7648-libvirt-unable-restore-snapshot: -------------------------------------------------------------------------------- 1 | Traceback (most recent call last): 2 | File "test/check-machines-snapshots", line *, in testSnapshotRevert 3 | b.wait_not_present("#vm-subVmTest1-snapshot-1-current") 4 | * 5 | File "test/common/testlib.py", line *, in wait_not_present 6 | self.wait_js_func('!ph_is_present', selector) 7 | * 8 | File "test/common/testlib.py", line *, in wait_js_func 9 | self.wait_js_cond("%s(%s)" % (func, ','.join(map(jsquote, args)))) 10 | * 11 | File "test/common/testlib.py", line *, in wait_js_cond 12 | raise Error(f"timeout\nwait_js_cond({cond}): {last_error.msg}") from None 13 | testlib.Error: timeout 14 | wait_js_cond(!ph_is_present("#vm-subVmTest1-snapshot-1-current")): Error: condition did not become true 15 | -------------------------------------------------------------------------------- /naughty/centos-10: -------------------------------------------------------------------------------- 1 | rhel-10 -------------------------------------------------------------------------------- /naughty/centos-8-stream: -------------------------------------------------------------------------------- 1 | rhel-8 -------------------------------------------------------------------------------- /naughty/centos-9-bootc: -------------------------------------------------------------------------------- 1 | rhel-9 -------------------------------------------------------------------------------- /naughty/centos-9-stream: -------------------------------------------------------------------------------- 1 | rhel-9 -------------------------------------------------------------------------------- /naughty/debian-stable/2463-no-pod-events: -------------------------------------------------------------------------------- 1 | wait_js_cond(ph_is_present("#table-pod-1 .pf*-c-empty-state")): 2 | -------------------------------------------------------------------------------- /naughty/debian-stable/2463-no-pod-events-1: -------------------------------------------------------------------------------- 1 | wait_js_cond(ph_is_present("#containers-containers .pod-name:contains('pod_user')")) 2 | -------------------------------------------------------------------------------- /naughty/debian-stable/2463-no-pod-events-2: -------------------------------------------------------------------------------- 1 | testCreateContainerInPodSystem 2 | * 3 | wait_js_cond(ph_is_present("#table-system_pod .create-container-in-pod 4 | -------------------------------------------------------------------------------- /naughty/debian-stable/2485-ipa-leave-crash: -------------------------------------------------------------------------------- 1 | warn*: Failed to leave domain: Running ipa-client-install failed 2 | * 3 | Traceback (most recent call last): 4 | File "test/verify/check-system-realms", line *, in testQualifiedUsers 5 | b.wait_not_present("#realms-leave-dialog") 6 | * 7 | testlib.Error: timeout 8 | -------------------------------------------------------------------------------- /naughty/debian-testing/2463-no-pod-events: -------------------------------------------------------------------------------- 1 | wait_js_cond(ph_is_present("#table-pod-1 .pf*-c-empty-state")): 2 | -------------------------------------------------------------------------------- /naughty/debian-testing/2463-no-pod-events-1: -------------------------------------------------------------------------------- 1 | wait_js_cond(ph_is_present("#containers-containers .pod-name:contains('pod_user')")) 2 | -------------------------------------------------------------------------------- /naughty/debian-testing/2463-no-pod-events-2: -------------------------------------------------------------------------------- 1 | testCreateContainerInPodSystem 2 | * 3 | wait_js_cond(ph_is_present("#table-system_pod .create-container-in-pod 4 | -------------------------------------------------------------------------------- /naughty/debian-testing/2485-ipa-leave-crash: -------------------------------------------------------------------------------- 1 | warn*: Failed to leave domain: Running ipa-client-install failed 2 | * 3 | Traceback (most recent call last): 4 | File "test/verify/check-system-realms", line *, in testQualifiedUsers 5 | b.wait_not_present("#realms-leave-dialog") 6 | * 7 | testlib.Error: timeout 8 | -------------------------------------------------------------------------------- /naughty/debian-testing/5364-apparmor-sysfs-zoned: -------------------------------------------------------------------------------- 1 | apparmor="DENIED" operation="open" class="file" profile="libvirt*" name="/sys/*/queue/zoned" * comm="qemu-system-x86" requested_mask="r" denied_mask="r" 2 | -------------------------------------------------------------------------------- /naughty/debian-testing/7646-libvirt-attach-disk-segfault: -------------------------------------------------------------------------------- 1 | Traceback (most recent call last): 2 | File "test/machineslib.py", line *, in tearDown 3 | super().tearDown() 4 | * 5 | File "test/common/testlib.py", line *, in tearDown 6 | self.check_journal_messages() 7 | * 8 | File "test/common/testlib.py", line *, in check_journal_messages 9 | raise Error(UNEXPECTED_MESSAGE + "journal messages:\n" + first) 10 | testlib.Error: FAIL: Test completed, but found unexpected journal messages: 11 | Process * (libvirtd) of user * terminated abnormally with signal 11/SEGV, processing... 12 | -------------------------------------------------------------------------------- /naughty/debian-testing/7648-libvirt-unable-restore-snapshot: -------------------------------------------------------------------------------- 1 | Traceback (most recent call last): 2 | File "test/check-machines-snapshots", line *, in testSnapshotRevert 3 | b.wait_not_present("#vm-subVmTest1-snapshot-1-current") 4 | * 5 | File "test/common/testlib.py", line *, in wait_not_present 6 | self.wait_js_func('!ph_is_present', selector) 7 | * 8 | File "test/common/testlib.py", line *, in wait_js_func 9 | self.wait_js_cond("%s(%s)" % (func, ','.join(map(jsquote, args)))) 10 | * 11 | File "test/common/testlib.py", line *, in wait_js_cond 12 | raise Error(f"timeout\nwait_js_cond({cond}): {last_error.msg}") from None 13 | testlib.Error: timeout 14 | wait_js_cond(!ph_is_present("#vm-subVmTest1-snapshot-1-current")): Error: condition did not become true 15 | -------------------------------------------------------------------------------- /naughty/example/123-log-and-traceback: -------------------------------------------------------------------------------- 1 | > log: phase coils are misaligned by* 2 | Traceback (most recent call last): 3 | File "test/verify/check-warp-drive", line *, in testCoils 4 | -------------------------------------------------------------------------------- /naughty/example/9876-example-traceback: -------------------------------------------------------------------------------- 1 | /usr/*/cockpit-pcp: bridge was killed: 2 | -------------------------------------------------------------------------------- /naughty/fedora-41/3683-selinux-agetty-clhm: -------------------------------------------------------------------------------- 1 | testlib.Error: FAIL: Test completed, but found unexpected journal messages: 2 | audit: type=1400 audit*: avc: denied { read } for * comm="agetty" name="22_clhm_*.issue" * scontext=system_u:system_r:getty_t:s0-s0:c0.c1023 tcontext=system_u:object_r:NetworkManager_dispatcher_console_var_run_t:s0* 3 | -------------------------------------------------------------------------------- /naughty/fedora-41/4796-stratis-runs-clevis-too-early: -------------------------------------------------------------------------------- 1 | File "check-storage-stratis", line *, in testBasic 2 | b.wait_text(self.card_row_col("Stratis filesystems", 1, 1), "fsys1") # should be started after boot 3 | -------------------------------------------------------------------------------- /naughty/fedora-41/6678-selinux-libvirt-ssh: -------------------------------------------------------------------------------- 1 | Traceback (most recent call last): 2 | File "test/check-machines-migrate", line *, in test* 3 | self._testMigrationGeneric(* 4 | File "test/check-machines-migrate", line *, in _testMigrationGeneric 5 | b.wait_not_present("#migrate-modal") 6 | * 7 | testlib.Error: timeout 8 | -------------------------------------------------------------------------------- /naughty/fedora-41/6769-kdump-initramfs-unpack-error: -------------------------------------------------------------------------------- 1 | [*] Initramfs unpacking failed: write error 2 | * 3 | File "check-kdump", line *, in testBasic 4 | self.assertIn("Kdump compressed dump", 5 | * 6 | AssertionError: 'Kdump compressed dump' not found in "/srv/kdump/var/crash/10.111.113.1*/vmcore: cannot open `/srv/kdump/var/crash/10.111.113.1*/vmcore' (No such file or directory)\n" 7 | -------------------------------------------------------------------------------- /naughty/fedora-41/6992-firefox-hidden-canvas-bug: -------------------------------------------------------------------------------- 1 | # testHistory (__main__.TestPages.testHistory) 2 | * 3 | > error: NS_ERROR_FAILURE:* 4 | * 5 | AssertionError: Cockpit shows an Oops 6 | -------------------------------------------------------------------------------- /naughty/fedora-41/7629-kdump-initrd-generation: -------------------------------------------------------------------------------- 1 | Traceback (most recent call last): 2 | File "check-kdump", line *, in testBasic 3 | b.wait_not_present(pathInput) 4 | -------------------------------------------------------------------------------- /naughty/fedora-41/7631-stratis-crypto-pool-boot: -------------------------------------------------------------------------------- 1 | Traceback (most recent call last): 2 | File "check-storage-stratis", line *, in testEncrypted 3 | b.wait_text(self.card_desc("Encrypted Stratis pool", "Name"), "pool0") 4 | -------------------------------------------------------------------------------- /naughty/fedora-41/7716-checkpoint-restore-failure: -------------------------------------------------------------------------------- 1 | Traceback (most recent call last): 2 | File "test/check-application", line *, in testCheckpointRestore 3 | b.wait(lambda: self.getContainerAttr("swamped-crate", "State") in NOT_RUNNING) 4 | ~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 5 | File "*", line *, in wait 6 | raise Error('timed out waiting for predicate to become true') 7 | testlib.Error: timed out waiting for predicate to become true 8 | -------------------------------------------------------------------------------- /naughty/fedora-41/7765-kdump-ansible-crashkernel-size: -------------------------------------------------------------------------------- 1 | Traceback (most recent call last): 2 | File "test/verify/check-kdump", line *, in testBasic 3 | kdump_machine.execute("until systemctl is-active kdump; do sleep 1; done") 4 | * 5 | RuntimeError: Timed out on 'until systemctl is-active kdump; do sleep 1; done' 6 | -------------------------------------------------------------------------------- /naughty/fedora-41/7765-kdump-crashkernel-size: -------------------------------------------------------------------------------- 1 | Traceback (most recent call last): 2 | File "test/verify/check-kdump", line *, in testBasic 3 | m.execute("until systemctl is-active kdump; do sleep 1; done") 4 | * 5 | RuntimeError: Timed out on 'until systemctl is-active kdump; do sleep 1; done' 6 | -------------------------------------------------------------------------------- /naughty/fedora-41/7765-kdump-crashkernel-size-2: -------------------------------------------------------------------------------- 1 | Traceback (most recent call last): 2 | File "check-kdump", line *, in testBasic 3 | b.wait_visible(".pf-v6-c-switch__input:checked") 4 | -------------------------------------------------------------------------------- /naughty/fedora-42/3683-selinux-agetty-clhm: -------------------------------------------------------------------------------- 1 | testlib.Error: FAIL: Test completed, but found unexpected journal messages: 2 | audit: type=1400 audit*: avc: denied { read } for * comm="agetty" name="22_clhm_*.issue" * scontext=system_u:system_r:getty_t:s0-s0:c0.c1023 tcontext=system_u:object_r:NetworkManager_dispatcher_console_var_run_t:s0* 3 | -------------------------------------------------------------------------------- /naughty/fedora-42/4796-stratis-runs-clevis-too-early: -------------------------------------------------------------------------------- 1 | File "check-storage-stratis", line *, in testBasic 2 | b.wait_text(self.card_row_col("Stratis filesystems", 1, 1), "fsys1") # should be started after boot 3 | -------------------------------------------------------------------------------- /naughty/fedora-42/6678-selinux-libvirt-ssh: -------------------------------------------------------------------------------- 1 | Traceback (most recent call last): 2 | File "test/check-machines-migrate", line *, in test* 3 | self._testMigrationGeneric(* 4 | File "test/check-machines-migrate", line *, in _testMigrationGeneric 5 | b.wait_not_present("#migrate-modal") 6 | * 7 | testlib.Error: timeout 8 | -------------------------------------------------------------------------------- /naughty/fedora-42/6769-kdump-initramfs-unpack-error: -------------------------------------------------------------------------------- 1 | [*] Initramfs unpacking failed: write error 2 | * 3 | File "check-kdump", line *, in testBasic 4 | self.assertIn("Kdump compressed dump", 5 | * 6 | AssertionError: 'Kdump compressed dump' not found in "/srv/kdump/var/crash/10.111.113.1*/vmcore: cannot open `/srv/kdump/var/crash/10.111.113.1*/vmcore' (No such file or directory)\n" 7 | -------------------------------------------------------------------------------- /naughty/fedora-42/6992-firefox-hidden-canvas-bug: -------------------------------------------------------------------------------- 1 | # testHistory (__main__.TestPages.testHistory) 2 | * 3 | > error: NS_ERROR_FAILURE:* 4 | * 5 | AssertionError: Cockpit shows an Oops 6 | -------------------------------------------------------------------------------- /naughty/fedora-42/7629-kdump-initrd-generation: -------------------------------------------------------------------------------- 1 | Traceback (most recent call last): 2 | File "check-kdump", line *, in testBasic 3 | b.wait_not_present(pathInput) 4 | -------------------------------------------------------------------------------- /naughty/fedora-42/7629-kdump-initrd-generation-2: -------------------------------------------------------------------------------- 1 | Traceback (most recent call last): 2 | File "check-kdump", line *, in testBasic 3 | b.wait_visible(".pf-v6-c-switch__input:checked") 4 | -------------------------------------------------------------------------------- /naughty/fedora-42/7629-kdump-initrd-generation-3: -------------------------------------------------------------------------------- 1 | Traceback (most recent call last): 2 | File "test/verify/check-kdump", line *, in testBasic 3 | * 4 | wait_js_cond(!ph_is_present(".pf-v6-c-alert__title")): Error: condition did not become true 5 | -------------------------------------------------------------------------------- /naughty/fedora-42/7631-stratis-crypto-pool-boot: -------------------------------------------------------------------------------- 1 | Traceback (most recent call last): 2 | File "check-storage-stratis", line *, in testEncrypted 3 | b.wait_text(self.card_desc("Encrypted Stratis pool", "Name"), "pool0") 4 | -------------------------------------------------------------------------------- /naughty/fedora-42/7716-checkpoint-restore-failure: -------------------------------------------------------------------------------- 1 | Traceback (most recent call last): 2 | File "test/check-application", line *, in testCheckpointRestore 3 | b.wait(lambda: self.getContainerAttr("swamped-crate", "State") in NOT_RUNNING) 4 | ~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 5 | File "*", line *, in wait 6 | raise Error('timed out waiting for predicate to become true') 7 | testlib.Error: timed out waiting for predicate to become true 8 | -------------------------------------------------------------------------------- /naughty/fedora-42/7765-kdump-ansible-crashkernel-size: -------------------------------------------------------------------------------- 1 | Traceback (most recent call last): 2 | File "test/verify/check-kdump", line *, in testBasic 3 | kdump_machine.execute("until systemctl is-active kdump; do sleep 1; done") 4 | * 5 | RuntimeError: Timed out on 'until systemctl is-active kdump; do sleep 1; done' 6 | -------------------------------------------------------------------------------- /naughty/fedora-42/7765-kdump-crashkernel-size: -------------------------------------------------------------------------------- 1 | Traceback (most recent call last): 2 | File "test/verify/check-kdump", line *, in testBasic 3 | m.execute("until systemctl is-active kdump; do sleep 1; done") 4 | * 5 | RuntimeError: Timed out on 'until systemctl is-active kdump; do sleep 1; done' 6 | -------------------------------------------------------------------------------- /naughty/fedora-42/7765-kdump-crashkernel-size-2: -------------------------------------------------------------------------------- 1 | Traceback (most recent call last): 2 | File "check-kdump", line *, in testBasic 3 | b.wait_visible(".pf-v6-c-switch__input:checked") 4 | -------------------------------------------------------------------------------- /naughty/fedora-43/3683-selinux-agetty-clhm: -------------------------------------------------------------------------------- 1 | testlib.Error: FAIL: Test completed, but found unexpected journal messages: 2 | audit: type=1400 audit*: avc: denied { read } for * comm="agetty" name="22_clhm_*.issue" * scontext=system_u:system_r:getty_t:s0-s0:c0.c1023 tcontext=system_u:object_r:NetworkManager_dispatcher_console_var_run_t:s0* 3 | -------------------------------------------------------------------------------- /naughty/fedora-43/4796-stratis-runs-clevis-too-early: -------------------------------------------------------------------------------- 1 | File "check-storage-stratis", line *, in testBasic 2 | b.wait_text(self.card_row_col("Stratis filesystems", 1, 1), "fsys1") # should be started after boot 3 | -------------------------------------------------------------------------------- /naughty/fedora-43/6678-selinux-libvirt-ssh: -------------------------------------------------------------------------------- 1 | Traceback (most recent call last): 2 | File "test/check-machines-migrate", line *, in test* 3 | self._testMigrationGeneric(* 4 | File "test/check-machines-migrate", line *, in _testMigrationGeneric 5 | b.wait_not_present("#migrate-modal") 6 | * 7 | testlib.Error: timeout 8 | -------------------------------------------------------------------------------- /naughty/fedora-43/6769-kdump-initramfs-unpack-error: -------------------------------------------------------------------------------- 1 | [*] Initramfs unpacking failed: write error 2 | * 3 | File "check-kdump", line *, in testBasic 4 | self.assertIn("Kdump compressed dump", 5 | * 6 | AssertionError: 'Kdump compressed dump' not found in "/srv/kdump/var/crash/10.111.113.1*/vmcore: cannot open `/srv/kdump/var/crash/10.111.113.1*/vmcore' (No such file or directory)\n" 7 | -------------------------------------------------------------------------------- /naughty/fedora-43/6992-firefox-hidden-canvas-bug: -------------------------------------------------------------------------------- 1 | # testHistory (__main__.TestPages.testHistory) 2 | * 3 | > error: NS_ERROR_FAILURE:* 4 | * 5 | AssertionError: Cockpit shows an Oops 6 | -------------------------------------------------------------------------------- /naughty/fedora-43/7648-libvirt-unable-restore-snapshot: -------------------------------------------------------------------------------- 1 | Traceback (most recent call last): 2 | File "test/check-machines-snapshots", line *, in testSnapshotRevert 3 | b.wait_not_present("#vm-subVmTest1-snapshot-1-current") 4 | * 5 | File "test/common/testlib.py", line *, in wait_not_present 6 | self.wait_js_func('!ph_is_present', selector) 7 | * 8 | File "test/common/testlib.py", line *, in wait_js_func 9 | self.wait_js_cond("%s(%s)" % (func, ','.join(map(jsquote, args)))) 10 | * 11 | File "test/common/testlib.py", line *, in wait_js_cond 12 | raise Error(f"timeout\nwait_js_cond({cond}): {last_error.msg}") from None 13 | testlib.Error: timeout 14 | wait_js_cond(!ph_is_present("#vm-subVmTest1-snapshot-1-current")): Error: condition did not become true 15 | -------------------------------------------------------------------------------- /naughty/fedora-43/7707-blivet-parted-disk-not-found: -------------------------------------------------------------------------------- 1 | testlib.Error: timeout 2 | wait_js_cond(ph_in_text("#cockpit-storage-integration-check-storage-dialog","'biosboot' partition on MDRAID device SOMERAID found. Bootloader partitions on MDRAID devices are not supported.")):* 3 | -------------------------------------------------------------------------------- /naughty/fedora-43/7716-checkpoint-restore-failure: -------------------------------------------------------------------------------- 1 | Traceback (most recent call last): 2 | File "test/check-application", line *, in testCheckpointRestore 3 | b.wait(lambda: self.getContainerAttr("swamped-crate", "State") in NOT_RUNNING) 4 | ~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 5 | File "*", line *, in wait 6 | raise Error('timed out waiting for predicate to become true') 7 | testlib.Error: timed out waiting for predicate to become true 8 | -------------------------------------------------------------------------------- /naughty/fedora-coreos: -------------------------------------------------------------------------------- 1 | fedora-39 -------------------------------------------------------------------------------- /naughty/fedora-rawhide: -------------------------------------------------------------------------------- 1 | fedora-43 -------------------------------------------------------------------------------- /naughty/fedora-rawhide-boot: -------------------------------------------------------------------------------- 1 | fedora-43 -------------------------------------------------------------------------------- /naughty/opensuse-tumbleweed/7648-libvirt-unable-restore-snapshot: -------------------------------------------------------------------------------- 1 | Traceback (most recent call last): 2 | File "test/check-machines-snapshots", line *, in testSnapshotRevert 3 | b.wait_not_present("#vm-subVmTest1-snapshot-1-current") 4 | * 5 | File "test/common/testlib.py", line *, in wait_not_present 6 | self.wait_js_func('!ph_is_present', selector) 7 | * 8 | File "test/common/testlib.py", line *, in wait_js_func 9 | self.wait_js_cond("%s(%s)" % (func, ','.join(map(jsquote, args)))) 10 | * 11 | File "test/common/testlib.py", line *, in wait_js_cond 12 | raise Error(f"timeout\nwait_js_cond({cond}): {last_error.msg}") from None 13 | testlib.Error: timeout 14 | wait_js_cond(!ph_is_present("#vm-subVmTest1-snapshot-1-current")): Error: condition did not become true 15 | -------------------------------------------------------------------------------- /naughty/opensuse-tumbleweed/7700-qemuBlockThrottleFiltersDetach-crash: -------------------------------------------------------------------------------- 1 | # testAddDiskCustomPath (__main__.TestMachinesDisks.testAddDiskCustomPath) 2 | * 3 | #0 * qemuBlockThrottleFiltersDetach (libvirt_driver_qemu.so + *) 4 | #1 * qemuDomainAttachDiskGeneric (libvirt_driver_qemu.so + *) 5 | #2 * qemuDomainAttachDeviceLive (libvirt_driver_qemu.so + *) 6 | * 7 | testlib.Error: FAIL: Test completed, but found unexpected journal messages: 8 | -------------------------------------------------------------------------------- /naughty/rhel-10-0: -------------------------------------------------------------------------------- 1 | rhel-10 -------------------------------------------------------------------------------- /naughty/rhel-10-1: -------------------------------------------------------------------------------- 1 | rhel-10 -------------------------------------------------------------------------------- /naughty/rhel-10/2538-iso-over-https: -------------------------------------------------------------------------------- 1 | Unknown driver 'https' 2 | -------------------------------------------------------------------------------- /naughty/rhel-10/3683-selinux-agetty-clhm: -------------------------------------------------------------------------------- 1 | testlib.Error: FAIL: Test completed, but found unexpected journal messages: 2 | audit: type=1400 audit*: avc: denied { read } for * comm="agetty" name="22_clhm_*.issue" * scontext=system_u:system_r:getty_t:s0-s0:c0.c1023 tcontext=system_u:object_r:NetworkManager_dispatcher_console_var_run_t:s0* 3 | -------------------------------------------------------------------------------- /naughty/rhel-10/4796-stratis-runs-clevis-too-early: -------------------------------------------------------------------------------- 1 | File "check-storage-stratis", line *, in testBasic 2 | b.wait_text(self.card_row_col("Stratis filesystems", 1, 1), "fsys1") # should be started after boot 3 | -------------------------------------------------------------------------------- /naughty/rhel-10/6678-selinux-libvirt-ssh: -------------------------------------------------------------------------------- 1 | Traceback (most recent call last): 2 | File "test/check-machines-migrate", line *, in test* 3 | self._testMigrationGeneric(* 4 | File "test/check-machines-migrate", line *, in _testMigrationGeneric 5 | b.wait_not_present("#migrate-modal") 6 | * 7 | testlib.Error: timeout 8 | -------------------------------------------------------------------------------- /naughty/rhel-10/7629-kdump-initrd-generation: -------------------------------------------------------------------------------- 1 | Traceback (most recent call last): 2 | File "check-kdump", line *, in testBasic 3 | b.wait_not_present(pathInput) 4 | -------------------------------------------------------------------------------- /naughty/rhel-10/7648-libvirt-unable-restore-snapshot: -------------------------------------------------------------------------------- 1 | Traceback (most recent call last): 2 | File "test/check-machines-snapshots", line *, in testSnapshotRevert 3 | b.wait_not_present("#vm-subVmTest1-snapshot-1-current") 4 | * 5 | File "test/common/testlib.py", line *, in wait_not_present 6 | self.wait_js_func('!ph_is_present', selector) 7 | * 8 | File "test/common/testlib.py", line *, in wait_js_func 9 | self.wait_js_cond("%s(%s)" % (func, ','.join(map(jsquote, args)))) 10 | * 11 | File "test/common/testlib.py", line *, in wait_js_cond 12 | raise Error(f"timeout\nwait_js_cond({cond}): {last_error.msg}") from None 13 | testlib.Error: timeout 14 | wait_js_cond(!ph_is_present("#vm-subVmTest1-snapshot-1-current")): Error: condition did not become true 15 | -------------------------------------------------------------------------------- /naughty/rhel-10/7700-qemuBlockThrottleFiltersDetach-crash: -------------------------------------------------------------------------------- 1 | # testAddDiskCustomPath (__main__.TestMachinesDisks.testAddDiskCustomPath) 2 | * 3 | #0 * qemuBlockThrottleFiltersDetach (libvirt_driver_qemu.so + *) 4 | #1 * qemuDomainAttachDiskGeneric (libvirt_driver_qemu.so + *) 5 | #2 * qemuDomainAttachDeviceLive (libvirt_driver_qemu.so + *) 6 | * 7 | testlib.Error: FAIL: Test completed, but found unexpected journal messages: 8 | -------------------------------------------------------------------------------- /naughty/rhel-10/7716-checkpoint-restore-failure: -------------------------------------------------------------------------------- 1 | Traceback (most recent call last): 2 | File "test/check-application", line *, in testCheckpointRestore 3 | b.wait(lambda: self.getContainerAttr("swamped-crate", "State") in NOT_RUNNING) 4 | ~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 5 | File "*", line *, in wait 6 | raise Error('timed out waiting for predicate to become true') 7 | testlib.Error: timed out waiting for predicate to become true 8 | -------------------------------------------------------------------------------- /naughty/rhel-10/7765-kdump-crashkernel-size: -------------------------------------------------------------------------------- 1 | Traceback (most recent call last): 2 | File "test/verify/check-kdump", line *, in testBasic 3 | m.execute("until systemctl is-active kdump; do sleep 1; done") 4 | * 5 | RuntimeError: Timed out on 'until systemctl is-active kdump; do sleep 1; done' 6 | -------------------------------------------------------------------------------- /naughty/rhel-10/7765-kdump-crashkernel-size-2: -------------------------------------------------------------------------------- 1 | Traceback (most recent call last): 2 | File "check-kdump", line *, in testBasic 3 | b.wait_visible(".pf-v6-c-switch__input:checked") 4 | -------------------------------------------------------------------------------- /naughty/rhel-8-10: -------------------------------------------------------------------------------- 1 | rhel-8 -------------------------------------------------------------------------------- /naughty/rhel-8-8: -------------------------------------------------------------------------------- 1 | rhel-8 -------------------------------------------------------------------------------- /naughty/rhel-8/1374-libvirt-crashes-on-test-teardown: -------------------------------------------------------------------------------- 1 | *TestMachines* 2 | * 3 | Process * (libvirtd) of user * dumped core.* 4 | #1 * virCondWait (libvirt.so.0) 5 | #2 * virThreadPoolWorker (libvirt.so.0) 6 | #3 * virThreadHelper (libvirt.so.0) 7 | -------------------------------------------------------------------------------- /naughty/rhel-8/2412-unlocking-stratis-during-boot: -------------------------------------------------------------------------------- 1 | File "check-storage-stratis", line *, in testEncrypted 2 | m.*reboot() 3 | * 4 | *exceptions.Failure: Timeout waiting for system to reboot properly 5 | -------------------------------------------------------------------------------- /naughty/rhel-8/4796-stratis-runs-clevis-too-early: -------------------------------------------------------------------------------- 1 | File "test/verify/check-storage-stratis", line *, in testBasic 2 | self.wait_mounted(1, 1) # should be mounted after boot 3 | -------------------------------------------------------------------------------- /naughty/rhel-9-2: -------------------------------------------------------------------------------- 1 | rhel-9 -------------------------------------------------------------------------------- /naughty/rhel-9-4: -------------------------------------------------------------------------------- 1 | rhel-9 -------------------------------------------------------------------------------- /naughty/rhel-9-6: -------------------------------------------------------------------------------- 1 | rhel-9 -------------------------------------------------------------------------------- /naughty/rhel-9-7: -------------------------------------------------------------------------------- 1 | rhel-9 -------------------------------------------------------------------------------- /naughty/rhel-9/2538-iso-over-https: -------------------------------------------------------------------------------- 1 | Unknown driver 'https' 2 | -------------------------------------------------------------------------------- /naughty/rhel-9/3683-selinux-agetty-clhm: -------------------------------------------------------------------------------- 1 | testlib.Error: FAIL: Test completed, but found unexpected journal messages: 2 | audit: type=1400 audit*: avc: denied { read } for * comm="agetty" name="22_clhm_*.issue" * scontext=system_u:system_r:getty_t:s0-s0:c0.c1023 tcontext=system_u:object_r:NetworkManager_dispatcher_console_var_run_t:s0* 3 | -------------------------------------------------------------------------------- /naughty/rhel-9/4796-stratis-runs-clevis-too-early: -------------------------------------------------------------------------------- 1 | File "check-storage-stratis", line *, in testBasic 2 | b.wait_text(self.card_row_col("Stratis filesystems", 1, 1), "fsys1") # should be started after boot 3 | -------------------------------------------------------------------------------- /naughty/rhel-9/4796-stratis-runs-clevis-too-early-old: -------------------------------------------------------------------------------- 1 | File "test/verify/check-storage-stratis", line *, in testBasic 2 | self.wait_mounted(1, 1) # should be mounted after boot 3 | -------------------------------------------------------------------------------- /naughty/rhel-9/5090-lvresize-fails-with-stratis-signature: -------------------------------------------------------------------------------- 1 | > warn*: Error resizing logical volume: Process reported exit code 5: File system device usage is not available from libblkid. 2 | * 3 | File "check-storage-stratis", line *, in testPoolResize 4 | -------------------------------------------------------------------------------- /naughty/rhel-9/6769-kdump-initramfs-unpack-error: -------------------------------------------------------------------------------- 1 | [*] Initramfs unpacking failed: write error 2 | * 3 | File "check-kdump", line *, in testBasic 4 | self.assertIn("Kdump compressed dump", 5 | * 6 | AssertionError: 'Kdump compressed dump' not found in "/srv/kdump/var/crash/10.111.113.1*/vmcore: cannot open `/srv/kdump/var/crash/10.111.113.1*/vmcore' (No such file or directory)\n" 7 | -------------------------------------------------------------------------------- /naughty/rhel-9/7374-selinux-nmmeta: -------------------------------------------------------------------------------- 1 | testlib.Error: FAIL: Test completed, but found unexpected journal messages: 2 | *avc: denied { create } for * comm="NetworkManager" name="*.nmmeta~" scontext=system_u:system_r:NetworkManager_t:s0 tcontext=system_u:object_r:NetworkManager_etc_rw_t:s0 tclass=lnk_file permissive=0 3 | -------------------------------------------------------------------------------- /naughty/rhel-9/7765-kdump-crashkernel-size: -------------------------------------------------------------------------------- 1 | Traceback (most recent call last): 2 | File "test/verify/check-kdump", line *, in testBasic 3 | m.execute("until systemctl is-active kdump; do sleep 1; done") 4 | * 5 | RuntimeError: Timed out on 'until systemctl is-active kdump; do sleep 1; done' 6 | -------------------------------------------------------------------------------- /naughty/rhel-9/7765-kdump-crashkernel-size-2: -------------------------------------------------------------------------------- 1 | Traceback (most recent call last): 2 | File "check-kdump", line *, in testBasic 3 | b.wait_visible(".pf-v6-c-switch__input:checked") 4 | -------------------------------------------------------------------------------- /naughty/ubuntu-2204/2463-no-pod-events: -------------------------------------------------------------------------------- 1 | wait_js_cond(ph_is_present("#table-pod-1 .pf*-c-empty-state")): 2 | -------------------------------------------------------------------------------- /naughty/ubuntu-2204/2463-no-pod-events-1: -------------------------------------------------------------------------------- 1 | wait_js_cond(ph_is_present("#containers-containers .pod-name:contains('pod_user')")) 2 | -------------------------------------------------------------------------------- /naughty/ubuntu-2204/2463-no-pod-events-2: -------------------------------------------------------------------------------- 1 | testCreateContainerInPodSystem 2 | * 3 | wait_js_cond(ph_is_present("#table-system_pod .create-container-in-pod 4 | -------------------------------------------------------------------------------- /naughty/ubuntu-2204/2485-ipa-leave-crash: -------------------------------------------------------------------------------- 1 | warn*: Failed to leave domain: Running ipa-client-install failed 2 | * 3 | Traceback (most recent call last): 4 | File "test/verify/check-system-realms", line *, in testQualifiedUsers 5 | b.wait_not_present("#realms-leave-dialog") 6 | * 7 | testlib.Error: timeout 8 | -------------------------------------------------------------------------------- /naughty/ubuntu-2204/4829-podman-hang: -------------------------------------------------------------------------------- 1 | File "test/check-application", line *, in testPruneUnusedContainers* 2 | * 3 | RuntimeError: Timed out on ' 4 | podman run --name inpod --pod pod -tid localhost/test-busybox sh -c 'exit 1'' 5 | -------------------------------------------------------------------------------- /naughty/ubuntu-2204/4829-podman-hang-2: -------------------------------------------------------------------------------- 1 | File "test/check-application", line *, in testPruneUnusedContainers* 2 | * 3 | RuntimeError: Timed out on ' 4 | podman run --name inpodrunning --pod pod -tid localhost/test-busybox sh -c 'sleep infinity'' 5 | -------------------------------------------------------------------------------- /naughty/ubuntu-2204/4829-podman-hang-3: -------------------------------------------------------------------------------- 1 | File "test/check-application", line *, in _createPod 2 | * 3 | RuntimeError: Timed out on 'podman run -d --pod testpod1 --name test-pod-1 --stop-timeout 0* 4 | -------------------------------------------------------------------------------- /naughty/ubuntu-2204/4829-podman-hang-4: -------------------------------------------------------------------------------- 1 | # testPruneUnusedContainers* 2 | * 3 | ----- user containers ----- 4 | timeout: sending signal TERM to command* 5 | -------------------------------------------------------------------------------- /naughty/ubuntu-2204/4829-podman-hang-5: -------------------------------------------------------------------------------- 1 | # testCreatePod* 2 | * 3 | ----- user containers ----- 4 | timeout: sending signal TERM to command* 5 | -------------------------------------------------------------------------------- /naughty/ubuntu-2204/4829-podman-hang-6: -------------------------------------------------------------------------------- 1 | File "test/check-application", line * 2 | self.waitPodContainer(* 3 | * 4 | testlib.Error: timeout 5 | * 6 | File "test/check-application", line *, in tearDown 7 | self.execute(auth, "podman ps -a >&2") 8 | * 9 | RuntimeError: Timed out on 'podman ps -a >&2' 10 | -------------------------------------------------------------------------------- /naughty/ubuntu-2404/2485-ipa-leave-crash: -------------------------------------------------------------------------------- 1 | warn*: Failed to leave domain: Running ipa-client-install failed 2 | * 3 | Traceback (most recent call last): 4 | File "test/verify/check-system-realms", line *, in testQualifiedUsers 5 | b.wait_not_present("#realms-leave-dialog") 6 | * 7 | testlib.Error: timeout 8 | -------------------------------------------------------------------------------- /naughty/ubuntu-2404/5364-apparmor-sysfs-zoned: -------------------------------------------------------------------------------- 1 | apparmor="DENIED" operation="open" class="file" profile="libvirt*" name="/sys/*/queue/zoned" * comm="qemu-system-x86" requested_mask="r" denied_mask="r" 2 | -------------------------------------------------------------------------------- /naughty/ubuntu-2404/7432-netman-vs-netplan: -------------------------------------------------------------------------------- 1 | Traceback (most recent call last): 2 | File "check-networkmanager-bond", line *, in testPrimary 3 | b.wait_val("#network-bond-settings-primary-select", iface) 4 | -------------------------------------------------------------------------------- /naughty/ubuntu-stable/2485-ipa-leave-crash: -------------------------------------------------------------------------------- 1 | warn*: Failed to leave domain: Running ipa-client-install failed 2 | * 3 | Traceback (most recent call last): 4 | File "test/verify/check-system-realms", line *, in testQualifiedUsers 5 | b.wait_not_present("#realms-leave-dialog") 6 | * 7 | testlib.Error: timeout 8 | -------------------------------------------------------------------------------- /naughty/ubuntu-stable/5364-apparmor-sysfs-zoned: -------------------------------------------------------------------------------- 1 | apparmor="DENIED" operation="open" class="file" profile="libvirt*" name="/sys/*/queue/zoned" * comm="qemu-system-x86" requested_mask="r" denied_mask="r" 2 | -------------------------------------------------------------------------------- /naughty/ubuntu-stable/7432-netman-vs-netplan: -------------------------------------------------------------------------------- 1 | Traceback (most recent call last): 2 | File "check-networkmanager-bond", line *, in testPrimary 3 | b.wait_val("#network-bond-settings-primary-select", iface) 4 | -------------------------------------------------------------------------------- /naughty/ubuntu-stable/7692-criu-errors: -------------------------------------------------------------------------------- 1 | * Error (criu/vdso.c:*): vdso: Unexpected rt vDSO area bounds 2 | * Error (criu/vdso.c:*): vdso: Failed to fill self vdso symtable 3 | * File "check-application", line *, in testCheckpointRestore 4 | b.wait(lambda: self.getContainerAttr("swamped-crate", "State") in NOT_RUNNING) 5 | -------------------------------------------------------------------------------- /npm: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # This is a helper script which downloads the `node:alpine` container and runs 4 | # `npm` commands inside of it, as an alternative to installing and running 5 | # `npm` on the host. 6 | 7 | set -eu 8 | 9 | cmd_sh() { 10 | # Lots of cockpit developers are using toolbox, which can't recursively run 11 | # podman, but flatpak-spawn offers a nice workaround for that. 12 | if [ -f /run/.toolboxenv ]; then 13 | exec flatpak-spawn --host -- "$0" sh "$@" 14 | exit 1 15 | fi 16 | 17 | CACHE="cockpit-project-npm-cache-volume" 18 | IMAGE="docker.io/library/node:alpine" 19 | 20 | # make sure the node user can write to the cache volume 21 | podman run \ 22 | --rm \ 23 | --pull=always \ 24 | --volume "${CACHE}":/home/node/.npm:U \ 25 | "${IMAGE}" chown -R node:node /home/node >&2 26 | 27 | # do the actual work 28 | exec podman run \ 29 | --log-driver='none' \ 30 | --rm \ 31 | --init \ 32 | --user node \ 33 | --workdir /home/node \ 34 | --interactive \ 35 | --attach stdin \ 36 | --attach stdout \ 37 | --attach stderr \ 38 | --volume "${CACHE}":/home/node/.npm \ 39 | "${IMAGE}" /bin/sh -c "$1" 40 | } 41 | 42 | cmd_download() { 43 | if [ -t 1 ]; then 44 | echo 'This command outputs tar to stdout. Use `bots/npm install` instead.' 45 | exit 1 46 | fi 47 | 48 | cmd_sh ' 49 | set -eux 50 | tee package.json >/dev/null 51 | npm install --ignore-scripts >&2 & wait -n # allows the shell to catch SIGINT 52 | cp package.json node_modules/.package.json 53 | tar --directory=node_modules --create . 54 | ' 55 | } 56 | 57 | cmd_install() { 58 | rm -rf node_modules 59 | mkdir node_modules 60 | cmd_download < package.json | tar --directory node_modules --exclude '.git*' --extract 61 | cp node_modules/.package-lock.json package-lock.json 62 | } 63 | 64 | cmd_outdated() { 65 | cmd_sh ' 66 | set -eux 67 | tee package.json >/dev/null 68 | npm outdated '"$*"' & wait -n # allows the shell to catch SIGINT 69 | ' < package.json 70 | } 71 | 72 | cmd_prune() { 73 | : # npm install already produces a clean result each time 74 | } 75 | 76 | main() { 77 | if [ $# = 0 ]; then 78 | # don't list the "private" ones 79 | echo 'This command requires a subcommand: install outdated sh' 80 | exit 1 81 | fi 82 | 83 | local fname="$(printf 'cmd_%s' "$1" | tr '-' '_')" 84 | if ! type -t "${fname}" | grep -q function; then 85 | echo "Unknown subcommand '$1'" 86 | exit 1 87 | fi 88 | 89 | shift 90 | "${fname}" "$@" 91 | } 92 | 93 | main "$@" 94 | -------------------------------------------------------------------------------- /publish-queue: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # This file is part of Cockpit. 4 | # 5 | # Copyright (C) 2021 Red Hat, Inc. 6 | # 7 | # Cockpit is free software; you can redistribute it and/or modify it 8 | # under the terms of the GNU Lesser General Public License as published by 9 | # the Free Software Foundation; either version 2.1 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # Cockpit is distributed in the hope that it will be useful, but 13 | # WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 15 | # Lesser General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Lesser General Public License 18 | # along with Cockpit; If not, see . 19 | 20 | import argparse 21 | import sys 22 | 23 | import pika 24 | 25 | from task import distributed_queue 26 | 27 | 28 | def main() -> int: 29 | parser = argparse.ArgumentParser(description='Publish stdin data to an AMQP task queue') 30 | parser.add_argument('-q', '--queue', required=True, 31 | help='Queue name') 32 | parser.add_argument('--amqp', default=distributed_queue.DEFAULT_AMQP_SERVER, 33 | help='The host:port of the AMQP server to publish to (default: %(default)s)') 34 | parser.add_argument('--secrets-dir', default=distributed_queue.DEFAULT_SECRETS_DIR, 35 | help='Directory with ca.pem and amqp-client.{pem,key} (default: %(default)s)') 36 | parser.add_argument('--create', action='store_true', 37 | help='Create the queue if it does not exist yet') 38 | opts = parser.parse_args() 39 | 40 | with distributed_queue.DistributedQueue(opts.amqp, [opts.queue], secrets_dir=opts.secrets_dir, 41 | passive=not opts.create) as q: 42 | body = sys.stdin.read().strip() 43 | q.channel.basic_publish('', opts.queue, body=body, 44 | properties=pika.BasicProperties(priority=distributed_queue.MAX_PRIORITY)) 45 | 46 | return 0 47 | 48 | 49 | if __name__ == '__main__': 50 | sys.exit(main()) 51 | -------------------------------------------------------------------------------- /push-rewrite: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | 3 | # push-rewrite -- Force push to a PR after rewriting history, with benefits. 4 | # 5 | # This tool force pushes your local changes to origin but only if that 6 | # doesn't change any files. If that is the case, the tool will also 7 | # copy the test results over. 8 | # 9 | # The idea is that you use this tool after squashing fixups in a pull 10 | # request or after other similar last minute rewriting activity before 11 | # merging a pull request. Then you can merge the PR using the GitHub 12 | # UI and get a nice "merged" label for it, and the tests will still be 13 | # green. 14 | 15 | import argparse 16 | import subprocess 17 | import sys 18 | import time 19 | 20 | from lib.aio.jsonutil import JsonObject, get_dict, get_str 21 | from task import github, labels_of_pull 22 | 23 | 24 | def execute(*args: str) -> str: 25 | try: 26 | sys.stderr.write("+ " + " ".join(args) + "\n") 27 | output = subprocess.check_output(args, text=True) 28 | except subprocess.CalledProcessError as ex: 29 | sys.exit(ex.returncode) 30 | return output 31 | 32 | 33 | def git(*args: str) -> str: 34 | return execute("git", *args).strip() 35 | 36 | 37 | def find_pr_with_sha(api: github.GitHub, sha: str) -> JsonObject: 38 | pulls = api.pulls() 39 | for pull in pulls: 40 | if get_str(get_dict(pull, "head"), "sha") == sha: 41 | return pull 42 | sys.stderr.write(f"Could not find pull with revision {sha}.\n") 43 | sys.exit(1) 44 | 45 | 46 | def main() -> None: 47 | parser = argparse.ArgumentParser(description='Force push after a rewrite') 48 | parser.add_argument('--repo', help="The GitHub repository to work with", default=None) 49 | opts = parser.parse_args() 50 | 51 | local = git('rev-parse', 'HEAD') 52 | remote = git('rev-parse', 'HEAD@{push}') 53 | 54 | if local == remote: 55 | sys.exit('Nothing to push') 56 | 57 | if git("diff", "--stat", local, remote) != "": 58 | sys.exit('You have local changes, aborting.') 59 | 60 | api = github.GitHub(repo=opts.repo) 61 | old_statuses = api.statuses(remote) 62 | 63 | pull = find_pr_with_sha(api, remote) 64 | labels = labels_of_pull(pull) 65 | tests_disabled = "no-test" in labels or "[no-test]" in get_str(pull, "title") 66 | # needs no-test label to prevent tests bring triggered 67 | # we cannot set the label for ourselves without `repo` permission 68 | if not tests_disabled: 69 | sys.exit("Please set the 'no-test' label on the PR before trying this") 70 | 71 | git("push", "--force-with-lease") 72 | 73 | for n in range(100, 0, -1): 74 | try: 75 | if api.get("commits/%s" % local): 76 | break 77 | except RuntimeError as e: 78 | if "Unprocessable Entity" not in str(e) or n <= 1: 79 | raise 80 | print("(new commits not yet in the GiHub API...please stand by. *beep*)") 81 | time.sleep(1) 82 | 83 | for key in old_statuses: 84 | if old_statuses[key]["state"] != "pending": 85 | print("Copying results for %s" % old_statuses[key]["context"]) 86 | api.post("statuses/" + local, old_statuses[key]) 87 | 88 | # remove no-test label 89 | if not tests_disabled: 90 | api.delete("issues/%i/labels/no-test" % pull["number"]) 91 | 92 | 93 | if __name__ == '__main__': 94 | main() 95 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.mypy] 2 | strict = true 3 | follow_imports = 'silent' # https://github.com/python-lsp/pylsp-mypy/issues/81 4 | scripts_are_modules = true # allow checking all scripts in one invocation 5 | warn_return_any = false 6 | 7 | [[tool.mypy.overrides]] 8 | # things which may be unavailable when running checks 9 | module = [ 10 | 'libvirt', 11 | 'libvirt_qemu', 12 | 'nacl', 13 | 'pika.*', 14 | ] 15 | ignore_missing_imports = true 16 | 17 | [[tool.mypy.overrides]] 18 | # https://github.com/python/mypy/issues/11401 prevents us from enabling strict 19 | # mode for a given set of files, so instead, we disable the failing checks for 20 | # the files which aren't strictly typed. Hopefully this decreases with time. 21 | check_untyped_defs = false 22 | disallow_untyped_calls = false 23 | disallow_untyped_defs = false 24 | warn_return_any = false 25 | module = [ 26 | 'task', 27 | 28 | 'test_github', 29 | 'test_issue_scan', 30 | 'test_task', 31 | 'test_test_failure_policy', 32 | 'test_tests_scan', 33 | 34 | 'cockpit-lib-update', 35 | 'image-refresh', 36 | 'image-trigger', 37 | 'naughty-prune', 38 | 'npm-update', 39 | 'po-refresh', 40 | 'store-tests', 41 | 'tasks-container-update', 42 | 'tests-status', 43 | ] 44 | 45 | [tool.ruff] 46 | exclude = [ 47 | ".git/", 48 | ] 49 | line-length = 118 50 | preview = true 51 | target-version = 'py312' 52 | 53 | [tool.ruff.lint] 54 | select = [ 55 | "A", # flake8-builtins 56 | "B", # flake8-bugbear 57 | "C4", # flake8-comprehensions 58 | "D300", # pydocstyle: Forbid ''' in docstrings 59 | "DTZ", # flake8-datetimez 60 | "E", # pycodestyle 61 | "EXE", # flake8-executable 62 | "F", # pyflakes 63 | "G", # flake8-logging-format 64 | "I", # isort 65 | "ICN", # flake8-import-conventions 66 | "PLE", # pylint errors 67 | "ISC", # flake8-implicit-str-concat 68 | "PGH", # pygrep-hooks 69 | "PIE", # flake8-pie 70 | "PLE", # pylint errors 71 | "RSE", # flake8-raise 72 | "RUF", # ruff rules 73 | "T10", # flake8-debugger 74 | "TCH", # flake8-type-checking 75 | "UP032", # f-string 76 | "W", # warnings (mostly whitespace) 77 | "YTT", # flake8-2020 78 | ] 79 | 80 | [tool.pytest.ini_options] 81 | addopts = ["--cov-config=pyproject.toml"] # for subprocesses 82 | pythonpath = ["."] 83 | required_plugins = ["pytest-asyncio"] 84 | asyncio_mode = 'auto' 85 | 86 | [tool.coverage.run] 87 | branch = true 88 | 89 | [tool.coverage.report] 90 | show_missing = true 91 | skip_covered = true 92 | exclude_lines = [ 93 | "pragma: no cover", # default 94 | "raise NotImplementedError", 95 | ] 96 | -------------------------------------------------------------------------------- /recreate-dependabot-pr: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # This file is part of Cockpit. 4 | # 5 | # Copyright (C) 2024 Red Hat, Inc. 6 | # 7 | # Cockpit is free software; you can redistribute it and/or modify it 8 | # under the terms of the GNU Lesser General Public License as published by 9 | # the Free Software Foundation; either version 2.1 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # Cockpit is distributed in the hope that it will be useful, but 13 | # WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 15 | # Lesser General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Lesser General Public License 18 | # along with Cockpit; If not, see . 19 | 20 | # Update COCKPIT_REPO_COMMIT to cockpit HEAD automatically, defaults to 21 | # Makefile as input optionally the full path can be provided. (For example 22 | # Anaconda uses ui/webui/Makefile.am). 23 | 24 | import os 25 | import sys 26 | import time 27 | from typing import Optional 28 | 29 | import task 30 | from lib.aio.jsonutil import get_dict, get_int, get_str 31 | 32 | sys.dont_write_bytecode = True 33 | 34 | 35 | def main() -> None: 36 | assert os.getenv('GITHUB_BASE') is not None, 'GITHUB_BASE must be set' 37 | 38 | api = task.github.GitHub() 39 | for pull in api.pulls(state="open"): 40 | number = get_int(pull, 'number') 41 | if number is None: 42 | continue 43 | 44 | user = get_dict(pull, 'user') 45 | if get_str(user, 'login') != 'dependabot[bot]': 46 | continue 47 | 48 | pull_details = api.get(f'pulls/{number}') 49 | 50 | # Ignore dependabot PR's with multiple commits, they might be work in progress. 51 | if get_int(pull_details, 'commits') > 1: 52 | print(f'Skipping pull {number}, it is being worked on') 53 | continue 54 | 55 | mergeable: Optional[bool] = pull_details['mergeable'] 56 | # State is unknown, retry with a timeout 57 | if mergeable is None: 58 | for retry in range(5): 59 | pull_details = api.get(f'pulls/{number}') 60 | mergeable = pull_details['mergeable'] 61 | if mergeable is not None: 62 | break 63 | 64 | print(f'Retrying to obtain mergeable status for pull={number}, retry={retry}') 65 | time.sleep(60) 66 | else: 67 | print(f'Reached timeout mergeable status still unknown for pull={number}') 68 | return 69 | 70 | if mergeable: 71 | print(f'Skipping pull {number}, it is in a mergeable state') 72 | continue 73 | else: 74 | # Not mergeable and a dependabot PR, add a `node_modules` label and re-create the PR. 75 | task.label(pull_details, ('node_modules',)) # type: ignore[no-untyped-call] 76 | 77 | # Stop at the first dependabot PR as we can only land one at a time. 78 | break 79 | 80 | 81 | if __name__ == '__main__': 82 | main() 83 | -------------------------------------------------------------------------------- /s3-lifecycle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | # For example: ./s3-lifecycle --days 90 https://cockpit-logs.us-east-1.linodeobjects.com/ 4 | # NB: The policy gets applied once daily, at midnight. 5 | 6 | import argparse 7 | import base64 8 | import hashlib 9 | import logging 10 | import textwrap 11 | import urllib 12 | from pathlib import Path 13 | 14 | from lib import s3 15 | 16 | 17 | def main() -> None: 18 | parser = argparse.ArgumentParser(description='Gets (default) or sets (--days, --file) S3 lifecycle policy') 19 | group = parser.add_mutually_exclusive_group() 20 | group.add_argument('--days', type=int, help='Set a simple expiry policy, in days') 21 | group.add_argument('--file', type=Path, help='Set the expiry policy from the given XML file') 22 | parser.add_argument('url', metavar='URL', help='The S3 URL to set the policy on') 23 | args = parser.parse_args() 24 | 25 | lifecycle = urllib.parse.urlparse(args.url)._replace(query='lifecycle=') 26 | 27 | if args.days: 28 | # https://www.linode.com/docs/products/storage/object-storage/guides/lifecycle-policies/ 29 | xml = textwrap.dedent(f""" 30 | 31 | 32 | delete-all-objects 33 | 34 | 35 | 36 | Enabled 37 | 38 | {args.days} 39 | 40 | 41 | """) 42 | elif args.file: 43 | xml = args.file.read_text() 44 | else: 45 | with s3.urlopen(lifecycle) as response: 46 | print(response.read()) 47 | return 48 | 49 | # For some reason this request requires Content-MD5, even though we also SHA256 the body... 50 | data = xml.encode('ascii') 51 | md5 = hashlib.md5() 52 | md5.update(data) 53 | headers = {'Content-MD5': base64.b64encode(md5.digest()).decode('ascii')} 54 | 55 | with s3.urlopen(lifecycle, method='PUT', headers=headers, data=data) as response: 56 | print(response.status) 57 | 58 | 59 | if __name__ == '__main__': 60 | logging.basicConfig(level=logging.DEBUG) 61 | main() 62 | -------------------------------------------------------------------------------- /setup-deploy-keys: -------------------------------------------------------------------------------- 1 | #!/bin/sh -x 2 | 3 | # (Re-)generate all deploy keys on 4 | # https://github.com/cockpit-project/cockpit/settings/environments 5 | # 6 | # Your personal access token needs `public_repo` for this to work: 7 | # https://github.com/settings/tokens 8 | # 9 | # You might want this first: 10 | # dnf install python3-pynacl 11 | # 12 | # Note: this script doesn't delete old secrets, so if you make adjustments, 13 | # please do that manually. 14 | 15 | set -eu 16 | cd "$(realpath -m "$0"/..)" 17 | 18 | DRY_RUN="-v" 19 | if test -n "${1:-}"; then 20 | if test "$1" = "--dry-run" -o "$1" = "-n"; then 21 | DRY_RUN="-n" 22 | else 23 | echo "Unrecognised argument" 24 | exit 1 25 | fi 26 | fi 27 | 28 | deploy_to() { 29 | ./github-upload-secrets ${DRY_RUN} --deploy-to "$@" 30 | } 31 | 32 | # bots 33 | deploy_to cockpit-project/bots \ 34 | --deploy-from \ 35 | cockpit-project/bots/self/DEPLOY_KEY 36 | 37 | # cockpit 38 | deploy_to cockpit-project/cockpit \ 39 | --deploy-from \ 40 | cockpit-project/cockpit/npm-update/SELF_DEPLOY_KEY \ 41 | cockpit-project/cockpit/self/DEPLOY_KEY 42 | 43 | deploy_to cockpit-project/cockpit-weblate \ 44 | --deploy-from \ 45 | cockpit-project/cockpit/cockpit-weblate/DEPLOY_KEY 46 | 47 | deploy_to cockpit-project/cockpit-project.github.io \ 48 | --deploy-from \ 49 | cockpit-project/cockpit/website/DEPLOY_KEY 50 | 51 | # cockpit-machines 52 | deploy_to cockpit-project/cockpit-machines \ 53 | --deploy-from \ 54 | cockpit-project/cockpit-machines/npm-update/SELF_DEPLOY_KEY \ 55 | cockpit-project/cockpit-machines/self/DEPLOY_KEY 56 | 57 | deploy_to cockpit-project/cockpit-machines-weblate \ 58 | --deploy-from \ 59 | cockpit-project/cockpit-machines/cockpit-machines-weblate/DEPLOY_KEY 60 | 61 | # cockpit-podman 62 | deploy_to cockpit-project/cockpit-podman \ 63 | --deploy-from \ 64 | cockpit-project/cockpit-podman/npm-update/SELF_DEPLOY_KEY \ 65 | cockpit-project/cockpit-podman/self/DEPLOY_KEY 66 | 67 | deploy_to cockpit-project/cockpit-podman-weblate \ 68 | --deploy-from \ 69 | cockpit-project/cockpit-podman/cockpit-podman-weblate/DEPLOY_KEY 70 | 71 | # cockpit-composer 72 | 73 | deploy_to osbuild/cockpit-composer-weblate \ 74 | --deploy-from \ 75 | osbuild/cockpit-composer/cockpit-composer-weblate/DEPLOY_KEY 76 | 77 | # cockpit-files 78 | deploy_to cockpit-project/cockpit-files \ 79 | --deploy-from \ 80 | cockpit-project/cockpit-files/npm-update/SELF_DEPLOY_KEY \ 81 | cockpit-project/cockpit-files/self/DEPLOY_KEY 82 | 83 | deploy_to cockpit-project/cockpit-files-weblate \ 84 | --deploy-from \ 85 | cockpit-project/cockpit-files/cockpit-files-weblate/DEPLOY_KEY 86 | 87 | # shared 88 | deploy_to cockpit-project/node-cache \ 89 | --deploy-from \ 90 | cockpit-project/cockpit/npm-update/NODE_CACHE_DEPLOY_KEY \ 91 | cockpit-project/cockpit/node-cache/DEPLOY_KEY \ 92 | cockpit-project/cockpit-files/npm-update/NODE_CACHE_DEPLOY_KEY \ 93 | cockpit-project/cockpit-files/node-cache/DEPLOY_KEY \ 94 | cockpit-project/cockpit-machines/npm-update/NODE_CACHE_DEPLOY_KEY \ 95 | cockpit-project/cockpit-machines/node-cache/DEPLOY_KEY \ 96 | cockpit-project/cockpit-podman/npm-update/NODE_CACHE_DEPLOY_KEY \ 97 | cockpit-project/cockpit-podman/node-cache/DEPLOY_KEY 98 | 99 | # flathub 100 | deploy_to cockpit-project/org.cockpit_project.CockpitClient \ 101 | --deploy-from \ 102 | cockpit-project/cockpit/flathub/DEPLOY_KEY 103 | -------------------------------------------------------------------------------- /setup-deploy-keys-anaconda: -------------------------------------------------------------------------------- 1 | #!/bin/sh -x 2 | 3 | # (Re-)generate all deploy keys on 4 | # https://github.com/rhinstaller/anaconda-webui/settings/environments 5 | # 6 | # Your personal access token needs `public_repo` for this to work: 7 | # https://github.com/settings/tokens 8 | # 9 | # You might want this first: 10 | # dnf install python3-pynacl 11 | # 12 | # Note: this script doesn't delete old secrets, so if you make adjustments, 13 | # please do that manually. 14 | 15 | set -eu 16 | cd "$(realpath -m "$0"/..)" 17 | 18 | DRY_RUN="-v" 19 | if test -n "${1:-}"; then 20 | if test "$1" = "--dry-run" -o "$1" = "-n"; then 21 | DRY_RUN="-n" 22 | else 23 | echo "Unrecognised argument" 24 | exit 1 25 | fi 26 | fi 27 | 28 | deploy_to() { 29 | ./github-upload-secrets ${DRY_RUN} --deploy-to "$@" 30 | } 31 | 32 | 33 | # anaconda-webui 34 | deploy_to rhinstaller/anaconda-webui \ 35 | --deploy-from \ 36 | rhinstaller/anaconda-webui/npm-update/SELF_DEPLOY_KEY \ 37 | rhinstaller/anaconda-webui/self/DEPLOY_KEY 38 | 39 | 40 | deploy_to rhinstaller/anaconda-webui-l10n \ 41 | --deploy-from \ 42 | rhinstaller/anaconda-webui/anaconda-webui-l10n/DEPLOY_KEY 43 | 44 | # shared 45 | deploy_to rhinstaller/node-cache \ 46 | --deploy-from \ 47 | rhinstaller/anaconda-webui/npm-update/NODE_CACHE_DEPLOY_KEY \ 48 | rhinstaller/anaconda-webui/node-cache/DEPLOY_KEY 49 | -------------------------------------------------------------------------------- /task/cache.py: -------------------------------------------------------------------------------- 1 | # This file is part of Cockpit. 2 | # 3 | # Copyright (C) 2015 Red Hat, Inc. 4 | # 5 | # Cockpit is free software; you can redistribute it and/or modify it 6 | # under the terms of the GNU Lesser General Public License as published by 7 | # the Free Software Foundation; either version 2.1 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # Cockpit is distributed in the hope that it will be useful, but 11 | # WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | # Lesser General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Lesser General Public License 16 | # along with Cockpit; If not, see . 17 | 18 | # Shared GitHub code. When run as a script, we print out info about 19 | # our GitHub interacition. 20 | 21 | import json 22 | import os 23 | import stat 24 | import sys 25 | import tempfile 26 | import time 27 | import urllib.error 28 | import urllib.parse 29 | import urllib.request 30 | from typing import Generic, TypeVar 31 | 32 | __all__ = ( 33 | 'Cache', 34 | ) 35 | 36 | _T = TypeVar('_T') 37 | 38 | 39 | class Cache(Generic[_T]): 40 | def __init__(self, directory: str, lag: int | None = None): 41 | self.directory = directory 42 | self.pruned = False 43 | 44 | # Default to zero lag when command on command line 45 | if lag is None: 46 | if os.isatty(0): 47 | lag = 0 48 | else: 49 | lag = 60 50 | 51 | # The lag tells us how long to assume cached data is "current" 52 | self.lag = lag 53 | 54 | # The mark tells us that stuff before this time is not "current" 55 | self.marked: float = 0 56 | 57 | # Prune old expired data from the cache directory 58 | def prune(self) -> None: 59 | try: 60 | entries = os.scandir(self.directory) 61 | except FileNotFoundError: 62 | # it's OK if the cache directory was deleted 63 | return 64 | 65 | oldest = time.time() - 7 * 86400 # discard files older than one week 66 | for entry in entries: 67 | if entry.is_file() and entry.stat().st_mtime < oldest: 68 | try: 69 | os.remove(entry.path) 70 | except FileNotFoundError: 71 | # maybe it got pruned by another process 72 | pass 73 | except OSError as exc: 74 | sys.stderr.write(f"Failed to remove GitHub cache item {entry.path}: {exc}\n") 75 | 76 | # Read a resource from the cache or return None 77 | def read(self, resource: str) -> _T | None: 78 | path = os.path.join(self.directory, urllib.parse.quote(resource, safe='')) 79 | try: 80 | with open(path) as fp: 81 | return json.load(fp) 82 | except (OSError, ValueError): 83 | return None 84 | 85 | # Write a resource to the cache in an atomic way 86 | def write(self, resource: str, contents: _T) -> None: 87 | path = os.path.join(self.directory, urllib.parse.quote(resource, safe='')) 88 | os.makedirs(self.directory, exist_ok=True) 89 | (fd, temp) = tempfile.mkstemp(dir=self.directory) 90 | with os.fdopen(fd, 'w') as fp: 91 | json.dump(contents, fp) 92 | os.chmod(temp, stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH) 93 | os.rename(temp, path) 94 | if not self.pruned: 95 | self.pruned = True 96 | self.prune() 97 | 98 | # Tell the cache that stuff before this time is not "current" 99 | def mark(self, mtime: float | None = None) -> None: 100 | if mtime is None: 101 | mtime = time.time() 102 | self.marked = mtime 103 | 104 | # Check if a given resource in the cache is "current" or not 105 | def current(self, resource: str) -> bool: 106 | path = os.path.join(self.directory, urllib.parse.quote(resource, safe='')) 107 | try: 108 | mtime = os.path.getmtime(path) 109 | return mtime > self.marked and mtime > (time.time() - self.lag) 110 | except OSError: 111 | return False 112 | -------------------------------------------------------------------------------- /task/test_mock_server.py: -------------------------------------------------------------------------------- 1 | # This file is part of Cockpit. 2 | # 3 | # Copyright (C) 2023 Red Hat, Inc. 4 | # 5 | # Cockpit is free software; you can redistribute it and/or modify it 6 | # under the terms of the GNU Lesser General Public License as published by 7 | # the Free Software Foundation; either version 2.1 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # Cockpit is distributed in the hope that it will be useful, but 11 | # WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | # Lesser General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Lesser General Public License 16 | # along with Cockpit; If not, see . 17 | 18 | import http.server 19 | import json 20 | import multiprocessing 21 | from collections.abc import Mapping 22 | 23 | 24 | class HTTPServer(http.server.HTTPServer): 25 | reply_count = 0 26 | data: object 27 | 28 | 29 | class MockServer: 30 | def __init__( 31 | self, address: tuple[str, int], handler: type[http.server.BaseHTTPRequestHandler], data: object = None 32 | ): 33 | self.address = address 34 | self.handler = handler 35 | self.data = data 36 | 37 | def run(self) -> None: 38 | srv = HTTPServer(self.address, self.handler) 39 | srv.data = self.data 40 | srv.serve_forever() 41 | 42 | def start(self) -> None: 43 | self.process = multiprocessing.Process(target=self.run) 44 | self.process.start() 45 | 46 | def kill(self) -> None: 47 | self.process.terminate() 48 | self.process.join() 49 | assert self.process.exitcode is not None 50 | 51 | 52 | class MockHandler(http.server.BaseHTTPRequestHandler): 53 | def replyData(self, value: str, headers: Mapping[str, str] = {}, status: int = 200) -> None: 54 | self.send_response(status) 55 | for name, content in headers.items(): 56 | self.send_header(name, content) 57 | self.end_headers() 58 | self.wfile.write(value.encode('utf-8')) 59 | self.wfile.flush() 60 | 61 | def replyJson(self, value: str, headers: Mapping[str, str] = {}, status: int = 200) -> None: 62 | assert isinstance(self.server, HTTPServer) 63 | self.server.reply_count += 1 64 | all_headers = {"Content-type": "application/json", **headers} 65 | self.replyData(json.dumps(value), headers=all_headers, status=status) 66 | -------------------------------------------------------------------------------- /tasks-container-update: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # This file is part of Cockpit. 4 | # 5 | # Copyright (C) 2024 Red Hat, Inc. 6 | # 7 | # Cockpit is free software; you can redistribute it and/or modify it 8 | # under the terms of the GNU Lesser General Public License as published by 9 | # the Free Software Foundation; either version 2.1 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # Cockpit is distributed in the hope that it will be useful, but 13 | # WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 15 | # Lesser General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Lesser General Public License 18 | # along with Cockpit; If not, see . 19 | 20 | # Update .cockpit-ci/container to the latest tasks container tag automatically 21 | 22 | import argparse 23 | import asyncio 24 | import logging 25 | import sys 26 | from pathlib import Path 27 | 28 | import aiohttp 29 | from yarl import URL 30 | 31 | import task 32 | from lib.aio.jsonutil import get_str, get_strv, typechecked 33 | 34 | sys.dont_write_bytecode = True 35 | 36 | 37 | async def main() -> None: 38 | parser = argparse.ArgumentParser() 39 | parser.add_argument('--debug', action='store_true', help="Print debugging messages") 40 | parser.add_argument('--dry-run', '-n', action='store_true', help="Don't push or open a PR") 41 | parser.add_argument('--image', default='ghcr.io/cockpit-project/tasks', help="The container image") 42 | parser.add_argument('--file', default='.cockpit-ci/container', help="The filename to write to") 43 | args = parser.parse_args() 44 | 45 | if args.debug: 46 | logging.basicConfig(level=logging.DEBUG) 47 | 48 | headers = { 49 | 'User-Agent': 'cockpit-project/bots (tasks-container-update)' 50 | } 51 | 52 | service, _, repository = args.image.partition('/') 53 | if not service or not repository: 54 | args.error(f'Invalid image name: {args.image}') 55 | 56 | async with aiohttp.ClientSession(raise_for_status=True) as session: 57 | async with session.get(URL.build(scheme='https', host=service, path='/token', query={ 58 | 'scope': f'repository:{repository}:pull', 59 | 'service': service 60 | }), headers=headers) as response: 61 | logging.debug('token response: %r', await response.json()) 62 | token = get_str(typechecked(await response.json(), dict), 'token') 63 | headers['Authorization'] = f'Bearer {token}' 64 | 65 | async with session.get(f'https://{service}/v2/{repository}/tags/list', headers=headers) as response: 66 | logging.debug('list response: %r', await response.json()) 67 | tags = get_strv(await response.json(), 'tags') 68 | 69 | tag = max({*tags} - {'latest'}) 70 | logging.debug('Latest tag is %r', tag) 71 | Path(args.file).write_text(f'{args.image}:{tag}\n') 72 | 73 | title = f"cockpit-ci: Update container to {tag}" 74 | branch = task.branch('cockpit-ci-container', title, pathspec=args.file, dry=args.dry_run) 75 | if branch is not None: 76 | task.pull(branch, title=title, dry=args.dry_run) 77 | 78 | 79 | if __name__ == '__main__': 80 | asyncio.run(main()) 81 | -------------------------------------------------------------------------------- /test/run: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eu 4 | 5 | find_scripts() { 6 | # Helper to find all scripts in the tree 7 | ( 8 | # Any non-binary file which contains a given shebang 9 | git grep --cached -lIz '^#!.*'"$1" 10 | shift 11 | # Any file matching the provided globs 12 | git ls-files -z "$@" 13 | ) | sort -z | uniq -z 14 | } 15 | 16 | find_python_files() { 17 | find_scripts 'python3' '*.py' 18 | } 19 | 20 | find_python_files | xargs -0 ruff check --quiet 21 | find_python_files | xargs -0 mypy --no-error-summary 22 | pytest -vv 23 | -------------------------------------------------------------------------------- /test/test_cache.py: -------------------------------------------------------------------------------- 1 | # This file is part of Cockpit. 2 | # 3 | # Copyright (C) 2017 Red Hat, Inc. 4 | # 5 | # Cockpit is free software; you can redistribute it and/or modify it 6 | # under the terms of the GNU Lesser General Public License as published by 7 | # the Free Software Foundation; either version 2.1 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # Cockpit is distributed in the hope that it will be useful, but 11 | # WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 | # Lesser General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Lesser General Public License 16 | # along with Cockpit; If not, see . 17 | 18 | import time 19 | from pathlib import Path 20 | 21 | from task import cache 22 | 23 | 24 | def test_read_write(tmp_path: Path) -> None: 25 | value = {"blah": 1} 26 | 27 | c = cache.Cache[object](f'{tmp_path}') 28 | result = c.read(r"pa+t\%h") 29 | assert result is None 30 | 31 | c.write(r"pa+t\%h", value) 32 | result = c.read(r"pa+t\%h") 33 | assert result == value 34 | 35 | other = "other" 36 | c.write(r"pa+t\%h", other) 37 | result = c.read(r"pa+t\%h") 38 | assert result == other 39 | 40 | c.write("second", value) 41 | result = c.read(r"pa+t\%h") 42 | assert result == other 43 | 44 | 45 | def test_current(tmp_path: Path) -> None: 46 | c = cache.Cache[object](f'{tmp_path}', lag=3) 47 | 48 | c.write("resource2", {"value": 2}) 49 | assert c.current('resource2') is True 50 | 51 | time.sleep(2) 52 | assert c.current('resource2') is True 53 | 54 | time.sleep(2) 55 | assert c.current('resource2') is False 56 | 57 | 58 | def test_current_mark(tmp_path: Path) -> None: 59 | c = cache.Cache[object](f'{tmp_path}', lag=3) 60 | 61 | assert c.current('resource') is False 62 | 63 | c.write("resource", {"value": 1}) 64 | assert c.current('resource') is True 65 | 66 | time.sleep(2) 67 | assert c.current('resource') is True 68 | 69 | c.mark() 70 | assert c.current('resource') is False 71 | 72 | 73 | def test_current_zero(tmp_path: Path) -> None: 74 | c = cache.Cache[object](f'{tmp_path}', lag=0) 75 | c.write("resource", {"value": 1}) 76 | assert c.current('resource') is False 77 | -------------------------------------------------------------------------------- /tests-status: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # This file is part of Cockpit. 4 | # 5 | # Copyright (C) 2020 Red Hat, Inc. 6 | # 7 | # Cockpit is free software; you can redistribute it and/or modify it 8 | # under the terms of the GNU Lesser General Public License as published by 9 | # the Free Software Foundation; either version 2.1 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # Cockpit is distributed in the hope that it will be useful, but 13 | # WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 15 | # Lesser General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU Lesser General Public License 18 | # along with Cockpit; If not, see . 19 | 20 | import argparse 21 | import subprocess 22 | import sys 23 | import time 24 | import urllib.parse 25 | import urllib.request 26 | 27 | import task 28 | from lib.network import host_ssl_context 29 | 30 | 31 | def print_summary(by_state, state): 32 | tests = by_state[state] 33 | print("%i tests in state %s: %s" % ( 34 | len(tests), 35 | state, 36 | " ".join([t[0] for t in tests]))) 37 | 38 | 39 | def print_failure(context, url): 40 | print(context + ":") 41 | print(" " + url) 42 | if url.endswith(".html"): 43 | url = url[:-5] 44 | with urllib.request.urlopen(url, context=host_ssl_context(urllib.parse.urlparse(url).netloc)) as f: 45 | for line in f: 46 | if line.startswith(b"not ok"): 47 | print(" " + line.strip().decode()) 48 | print() 49 | 50 | 51 | def git(*args): 52 | return subprocess.check_output(('git', *args), encoding='utf-8').strip() 53 | 54 | 55 | # returns a dict of state->[(context, url)] 56 | def sort_statuses(statuses): 57 | by_context = {} # context → (state, url) 58 | for context, status in statuses.items(): 59 | # latest status wins 60 | if context in by_context: 61 | continue 62 | by_context[context] = (status["state"], status.get("target_url", "")) 63 | 64 | by_state = {} # state → [(context, url), ..] 65 | for context, (state, url) in by_context.items(): 66 | by_state.setdefault(state, []).append((context, url)) 67 | 68 | return by_state 69 | 70 | 71 | def main(): 72 | parser = argparse.ArgumentParser(description='Summarize test status of a PR') 73 | parser.add_argument('--wait', action='store_true', help="Wait for all green, or one red", default=None) 74 | parser.add_argument('--repo', help="The repository of the PR", default=None) 75 | parser.add_argument('-v', '--verbose', action="store_true", default=False, 76 | help="Print verbose information") 77 | parser.add_argument("target", help='The pull request number to inspect, ' 78 | 'or - for the upstream of the current branch') 79 | opts = parser.parse_args() 80 | 81 | api = task.github.GitHub(repo=opts.repo) 82 | 83 | if opts.target != '-': 84 | pull = api.get(f"pulls/{opts.target}") 85 | if not pull: 86 | sys.exit(f"{opts.target} is not a pull request.") 87 | revision = pull['head']['sha'] 88 | else: 89 | revision = git('rev-parse', '@{upstream}') 90 | 91 | while True: 92 | by_state = sort_statuses(api.statuses(revision)) 93 | 94 | if 'pending' not in by_state or 'failure' in by_state or not opts.wait: 95 | break 96 | 97 | print_summary(by_state, 'pending') 98 | print('waiting...\n') 99 | time.sleep(30) 100 | 101 | for state in by_state.keys(): 102 | if state != "failure": 103 | print_summary(by_state, state) 104 | 105 | failed = by_state.get("failure") 106 | if not failed: 107 | return 108 | print("\nFailed tests\n============\n") 109 | for (context, url) in failed: 110 | print_failure(context, url) 111 | 112 | 113 | if __name__ == '__main__': 114 | main() 115 | -------------------------------------------------------------------------------- /vm-reset: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # This file is part of Cockpit. 3 | # 4 | # Copyright (C) 2013 Red Hat, Inc. 5 | # 6 | # Cockpit is free software; you can redistribute it and/or modify it 7 | # under the terms of the GNU Lesser General Public License as published by 8 | # the Free Software Foundation; either version 2.1 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # Cockpit is distributed in the hope that it will be useful, but 12 | # WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 14 | # Lesser General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU Lesser General Public License 17 | # along with Cockpit; If not, see . 18 | 19 | SELF=$(basename $0) 20 | BASE=$(dirname $(dirname $0)) 21 | 22 | usage() 23 | { 24 | echo >&2 "Usage: $SELF" 25 | } 26 | 27 | case ${1:-} in 28 | --help|-h) 29 | usage 30 | exit 0 31 | ;; 32 | esac 33 | 34 | rm -rf $BASE/tmp/run/* $BASE/tmp/run/.*?? $BASE/test/images/* 35 | --------------------------------------------------------------------------------