├── .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 |
--------------------------------------------------------------------------------