Compare commits

...

126 Commits

Author SHA1 Message Date
Stepan Oksanichenko 91d282708e
- Method `get_remote_file_content` is object's method now 2023-11-21 09:19:01 +02:00
Stepan Oksanichenko ccaf31bc87
- Method `get_remote_file_content` is object's method now 2023-11-21 08:51:05 +02:00
Stepan Oksanichenko 5fe0504265
- Spec's changelog chronology is fixed 2023-11-15 15:14:22 +02:00
Stepan Oksanichenko d79f163685
- Bump version 2023-11-15 14:49:51 +02:00
Stepan Oksanichenko 793fb23958
- Bump version 2023-11-15 14:02:10 +02:00
Stepan Oksanichenko 65d0c09e97
- Return empty list if a repo doesn't contain any module 2023-11-15 13:17:57 +02:00
Stepan Oksanichenko 0a9e5df66c
- Properly removing tmp files 2023-11-10 21:38:01 +02:00
Stepan Oksanichenko ae527a2e01
- The unittests are fixed 2023-11-10 18:08:03 +02:00
Aditya Bisoi 4991144a01
4.5.0 release
Signed-off-by: Aditya Bisoi <abisoi@redhat.com>

(cherry picked from commit 4c7611291d (centos_master))
2023-11-10 16:58:03 +02:00
Lubomír Sedlář 68d94ff488
kojiwrapper: Stop being smart about local access
Rather than trying to use local access when it's accessible, let user
make the decision:

 * if koji_cache is configured use it and download stuff
 * if not, fall back to local access

Signed-off-by: Lubomír Sedlář <lsedlar@redhat.com>
(cherry picked from commit 0d3cd150bd)
2023-11-10 16:57:53 +02:00
Ozan Unsal ce45fdc39a
Fix unittest errors
Signed-off-by: Ozan Unsal <ounsal@redhat.com>

(cherry picked from commit aa0aae3d3e (centos_master))
2023-11-10 16:57:51 +02:00
Lubomír Sedlář b625ccea06
Add integrity checking for builds
When a real build is downloaded, Koji can provide a checksum via API.
This commit adds verification of that checksum.

A mismatch will abort the compose. If Koji doesn't provide a checksum
for the particular sigkey, no checking will happen.

Nothing is still checked for scratch builds and images.

This patch requires Koji 1.32. When talking to an older version, there
is no checking done.

Signed-off-by: Lubomír Sedlář <lsedlar@redhat.com>
(cherry picked from commit 77f8fa25ad)
2023-11-10 16:55:44 +02:00
Lubomír Sedlář 8eccfc5a03
Add script for cleaning up the cache
Pungi would by default only ever add files to the cache. That would
eventually result in essentially a mirror of the Koji volume.

This patch adds a helper cleanup script. When called, it goes through
files in the cache and deletes anything that is not hardlinked from
elsewhere and with mtime not updated recently.

Cleaning up files that hardlinked from some compose would not save any
space anyway. The mtime check should account for cases like subpackage
being downloaded but not included in any compose. This would avoid it
from being downloaded over and over again.

When a compose fails or is aborted, there can be a stale lock file left
behind in the cache. This script cleans that up too.

Signed-off-by: Lubomír Sedlář <lsedlar@redhat.com>

(cherry picked from commit e6d9f31ef4 (centos_master))
2023-11-10 16:55:43 +02:00
Lubomír Sedlář f5a0e06af5
Add ability to download images
This patch extends the ability to download files from Koji to image
building phases too.

There is no integrity checking for the downloaded images.

Signed-off-by: Lubomír Sedlář <lsedlar@redhat.com>
(cherry picked from commit bf3e9bc53a)
2023-11-10 16:55:20 +02:00
Lubomír Sedlář f6f54b56ca
Add support for not having koji volume mounted locally
With this patch, Pungi can be configured with a local directory to be
used as a cache for RPMs, and it will download packages from Koji over
HTTP instead of reading them from filesystem directly.

The files from the cache can then be hardlink as usual.

There is locking in place to avoid different composes running at the
same time to step on each other.

This is now supported for RPMs only, be it real builds or scratch
builds.

Signed-off-by: Lubomír Sedlář <lsedlar@redhat.com>
(cherry picked from commit 631bb01d8f)
2023-11-10 16:55:19 +02:00
Aditya Bisoi fcee346c7c
Remove repository cloning multiple times
JIRA: RHELCMP-8913
Signed-off-by: Aditya Bisoi <abisoi@redhat.com>
(cherry picked from commit b6296bdfcd)
2023-11-10 16:55:18 +02:00
Lubomír Sedlář 82ec38ad60
Support require_all_comps_packages on DNF backend
It's not a great name anymore though, because it will fail the compose
if any input package is missing, no matter whether it's from comps,
prepopulate or additional_packages.

JIRA: RHELCMP-12484
Signed-off-by: Lubomír Sedlář <lsedlar@redhat.com>
(cherry picked from commit 1c4275bbfa)
2023-11-10 16:55:17 +02:00
Lubomír Sedlář c9cbd80569
Fix new warnings from flake8
Use isinstance rather than directly comparing types.

Signed-off-by: Lubomír Sedlář <lsedlar@redhat.com>
(cherry picked from commit fe2dad3b3c)
2023-11-10 16:55:16 +02:00
Aditya Bisoi 035fca1e6d
4.4.1 release
Signed-off-by: Aditya Bisoi <abisoi@redhat.com>

(cherry picked from commit 7128021654 (centos_master))
2023-11-10 16:55:15 +02:00
Lubomír Sedlář 0f8cae69b7
ostree: Add configuration for custom runroot packages
Signed-off-by: Lubomír Sedlář <lsedlar@redhat.com>
(cherry picked from commit bd64894a03)
2023-11-10 16:55:01 +02:00
Lubomír Sedlář f17628dd5f
pkgset: Emit better error for missing modulemd file
The exceptions from libmodulemd are not particularly helpful as they do
not contain information about what file caused it.

   modulemd-yaml-error-quark: Failed to open file: Permission denied (0)

This patch should add the path to the problematic file into the message.

Signed-off-by: Lubomír Sedlář <lsedlar@redhat.com>
(cherry picked from commit 14e025a5a1)
2023-11-10 16:55:00 +02:00
Lubomír Sedlář f3485410ad
Add support for git-credential-helper
This patch adds an additional field `options` to scm_dict, which can be
used to provide additional information to the backends.

It implements a single new option for GitWrapper. This option allows
setting a custom git credentials wrapper. This can be useful if Pungi
needs to get files from a git repository that requires authentication.

The helper can be as simple as this (assuming the username is already
provided in the url):

    #!/bin/sh
    echo password=i-am-secret

The helper would need to be referenced by an absolute path from the
pungi configuration, or prefixed with ! to have git interpret it as a
shell script and look it up in PATH.

See https://git-scm.com/docs/gitcredentials for more details.

Signed-off-by: Lubomír Sedlář <lsedlar@redhat.com>
JIRA: RHELCMP-11808
(cherry picked from commit ada8f4e346)
2023-11-10 16:54:59 +02:00
Haibo Lin cccfaea14e
Support OIDC Client Credentials authentication to CTS
JIRA: RHELCMP-11324
Signed-off-by: Haibo Lin <hlin@redhat.com>
(cherry picked from commit e4c525ecbf)
2023-11-10 16:54:58 +02:00
Lubomír Sedlář e2057b75c5
4.4.0 release
JIRA: RHELCMP-11764
Signed-off-by: Lubomír Sedlář <lsedlar@redhat.com>

(cherry picked from commit 091d228219 (centos_stream))
2023-11-10 16:54:57 +02:00
Lubomír Sedlář 44ea4d4419
gather-dnf: Run latest() later
The initial version of the filtered the latest builds at the start. That
doesn't matter in many cases:

* When there are no lookaside repos, there is generally a single version
  of each package.
* When lookaside repos do not overlap with compose repos, or contain
  only older versions.

It is however a problem when the lookaside repos contain higher version
of a package than what is in a compose repo, and some package explicitly
requires the older version.

Consider this scenario:

* lookaside contains bar-1.1
* compose repo contains bar-1.0 and foo-1.0
* foo-1.0 `Requires: bar < 1.1`

The original code would filter out the bar-1.0 package, and then fail on
unresolved dependencies.

This patch moves the computation of latest packages much later, to part
of code where all options to satisfy a dependency are selected and the
best match is chosen. At that point if there are multiple versions
available, we do want the latest one.

JIRA: SPMM-13483
Signed-off-by: Lubomír Sedlář <lsedlar@redhat.com>
(cherry picked from commit bcc440491e)
2023-11-10 16:54:43 +02:00
Lubomír Sedlář d4425f7935
iso: Support joliet long names
Without this option the names reported by joliet tree are truncated.

Signed-off-by: Lubomír Sedlář <lsedlar@redhat.com>
(cherry picked from commit fa50eedfad)
2023-11-10 16:54:42 +02:00
Lubomír Sedlář c8118527ea
Drop pungi-orchestrator code
This was never actually used.

JIRA: RHELCMP-10218
Signed-off-by: Lubomír Sedlář <lsedlar@redhat.com>

(cherry picked from commit b7adbf8a91 (centos_master))
2023-11-10 16:54:40 +02:00
Lubomír Sedlář a8ea322907
isos: Ensure proper file ownership and permissions
The genisoimage backend uses the -rational-rock option, which sets uid
and gid to 0, and makes file readable by everyone.

With xorriso this must be done explicitly. Setting ownership is a single
command, but the permissions require a per-file command to not make
files executable where not needed.

Fixes: https://bugzilla.redhat.com/show_bug.cgi?id=2203888
Signed-off-by: Lubomír Sedlář <lsedlar@redhat.com>

(cherry picked from commit 82ae9e86d5 (centos_master))
2023-11-10 16:54:22 +02:00
Lubomír Sedlář c4995c8f4b
gather: Always get latest packages
If lookaside contains an older version of a package, but with a
different arch, the depsolver doesn't notice that and prefers the
lookaside version.

This is not correct. The latest package should be used no matter if
there are different arches available.

The filtering in DNF doesn't ensure this, so we have to build it
ourselves. To limit the performance impact, only run this filtering when
there actually are some lookaside repos configured.

JIRA: RHELCMP-11728

Signed-off-by: Lubomír Sedlář <lsedlar@redhat.com>
(cherry picked from commit 2ad341a01c)
2023-11-10 16:54:01 +02:00
Lubomír Sedlář 997e372f25
Add back compatibility with jsonschema <3.0.0
Resolves: https://pagure.io/pungi/issue/1667
Signed-off-by: Lubomír Sedlář <lsedlar@redhat.com>

(cherry picked from commit e888e76992 (centos_master))
2023-11-10 16:54:00 +02:00
Lubomír Sedlář 42f1c62528
Remove useless debug message
Signed-off-by: Lubomír Sedlář <lsedlar@redhat.com>
(cherry picked from commit 6e72de7efe)
2023-11-10 16:52:27 +02:00
Lubomír Sedlář 3fd29d0ee0
Remove fedmsg from requirements
The code for sending messages in Fedora actually relies on
fedora-messaging library now. However, we do not have any tests for
that, so there's little reason to pull the library in via
requirements.txt

Signed-off-by: Lubomír Sedlář <lsedlar@redhat.com>
(cherry picked from commit c8263fcd39 (centos_master))
2023-11-10 16:52:04 +02:00
Lubomír Sedlář c1f2fa5035
gather: Support dotarch in DNF backend
The documentation claims that dotarch syntax is supported for additional
packages. For yum backend this seems to be handled automatically, but
the dnf backend could not interpret this.

This patch checks if a package is specified in the syntax and contains a
valid architecture. If so, the query will honor the arch.

JIRA: RHELCMP-11728
Signed-off-by: Lubomír Sedlář <lsedlar@redhat.com>
(cherry picked from commit 82ca4f4e65)
2023-11-10 16:51:55 +02:00
Aurélien Bompard 85c9e9e776
Set the priority in the fedora-messaging notifier
According to [infra ticket #10899](https://pagure.io/fedora-infrastructure/issue/10899),
ostree messages should have prioriy 3.

Signed-off-by: Aurélien Bompard <aurelien@bompard.org>
(cherry picked from commit b8b6b46ce7)
2023-11-10 16:51:54 +02:00
Lubomír Sedlář 33012ab31e
Fix compatibility with createrepo_c 0.21.1
The length of the file entry tuple has changed, it can not be unpacked
reliably.

Relates: https://github.com/rpm-software-management/createrepo_c/issues/360
Signed-off-by: Lubomír Sedlář <lsedlar@redhat.com>
(cherry picked from commit e9d836c115)
2023-11-10 16:51:53 +02:00
Lubomír Sedlář 72ddf65e62
comps: Apply arch filtering to environment/optionlist
Let's filter this list too, not just the grouplist tag.

JIRA: RHELCMP-7926
Signed-off-by: Lubomír Sedlář <lsedlar@redhat.com>
(cherry picked from commit d3f0701e01)
2023-11-10 16:51:52 +02:00
Haibo Lin c402ff3d60
Add config file for cleaning up cache files
systemd-tmpfiles is required to enable the auto clean up.

JIRA: RHELCMP-6327
Signed-off-by: Haibo Lin <hlin@redhat.com>
(cherry picked from commit 8f6f0f463f)
2023-11-10 16:51:51 +02:00
Haibo Lin 8dd344f9ee
4.3.8 release
JIRA: RHELCMP-11448
Signed-off-by: Haibo Lin <hlin@redhat.com>

(cherry picked from commit 467c7a7f6a (centos_master))
2023-11-10 16:51:49 +02:00
Lubomír Sedlář d07f517a90
createiso: Update possibly changed file on DVD
There's no good way of detecting if buildinstall phase tweaked boot
configuration (and efiboot.img). We should update those files in the DVD
just to be sure.

The .discinfo file is always different and needs to be updated.

Relates: https://pagure.io/pungi/issue/1647
JIRA: RHELCMP-10811
Signed-off-by: Lubomír Sedlář <lsedlar@redhat.com>
(cherry picked from commit e1d7544c2b)
2023-11-10 16:51:39 +02:00
Lubomír Sedlář 48366177cc
pkgset: Stop reuse if configuration changed
When options controlling excluding arches change, it should break reuse.

Signed-off-by: Lubomír Sedlář <lsedlar@redhat.com>
(cherry picked from commit a71c8e23be)
2023-11-10 16:51:38 +02:00
Lubomír Sedlář 4cb8671fe4
Allow disabling inheriting ExcludeArch to noarch packages
Copying ExcludeArch/ExclusiveArch from source rpm to noarch is an easy
option to block shipping that particular noarch package from a certain
architecture. However, there is no way to bypass it, and it is rather
confusing and not discoverable.

An alternative way to remove an unwanted package is to use the good old
`filter_packages`, which has enough granularity to remove pretty much
anything from anywhere. The only downside is that it requires a change
in configuration, so it can't be done by a packager directly from a spec
file.

When we decide to break backwards compatibility, this option should be
removed and the entire ExcludeArch/ExclusiveArch inheritance removed
completely.

JIRA: ENGCMP-2606
Signed-off-by: Lubomír Sedlář <lsedlar@redhat.com>
(cherry picked from commit ab508c1511)
2023-11-10 16:51:37 +02:00
Lubomír Sedlář 135bbbfe7e
pkgset: Support extra builds with no tags
This is a rather fringe use case. If the configuration contains
pkgset_koji_builds or pkgset_koji_scratch_tasks but no pkgset_koji_tag,
the compose will be empty.

The expectation though is that the packages should be pulled.

The extra RPMs are added to all non-modular tags because they are
supposed to mask builds from the same packages (e.g. user may want to
explicitly pull in older version than tagged).

This patch adds support for composes containing only explicitly listed
builds by creating a dummy package set that is not actually using any
tag.

JIRA: RHELCMP-11385
Signed-off-by: Lubomír Sedlář <lsedlar@redhat.com>
(cherry picked from commit f960b4d155)
2023-11-10 16:51:36 +02:00
Lubomír Sedlář 5624829564
buildinstall: Avoid pointlessly tweaking the boot images
Only modify boot images if there actually is some change.

The tweak function updates config files with volume id and kickstart
file. Even if we don't have a kickstart and there is no change in the
config files, the image will be regenerated. This leads to a change in
checksum for no good reason.

This patch keeps track of modified config files. If there are none, it
avoids touching anything else.

Signed-off-by: Lubomír Sedlář <lsedlar@redhat.com>
(cherry picked from commit 602b698080)
2023-11-10 16:51:35 +02:00
Haibo Lin 5fb4f86312
Prevent to reuse if unsigned packages are allowed
JIRA: RHELCMP-8415
Signed-off-by: Haibo Lin <hlin@redhat.com>
(cherry picked from commit b30f7e0d83)
2023-11-10 16:51:34 +02:00
Lubomír Sedlář e891fe7b09
Pass parent id/respin id to CTS
When the --target-dir option is used, the compose can be created in CTS,
but the parent and respin information is not passed through. That leads
to data missing later on.

JIRA: RHELCMP-11411
Signed-off-by: Lubomír Sedlář <lsedlar@redhat.com>

(cherry picked from commit 0c3b6e22f9 (centos_master))
2023-11-10 16:51:33 +02:00
Haibo Lin 4cd7d39914
Exclude existing files in boot.iso
JIRA: RHELCMP-10811
Fixes: https://pagure.io/pungi/issue/1647
Signed-off-by: Haibo Lin <hlin@redhat.com>
(cherry picked from commit 3175ede38a)
2023-11-10 16:50:46 +02:00
Lubomír Sedlář 5de829d05b
image-build/osbuild: Pull ISOs into the compose
OSBuild tasks can produce ISO files. If they do, we should include them
in the compose, and we should pull them into the iso/ subdirectory
together with other ISOs.

Fixes: https://pagure.io/pungi/issue/1657
Signed-off-by: Lubomír Sedlář <lsedlar@redhat.com>
(cherry picked from commit 8920eef339)
2023-11-10 16:50:45 +02:00
Lubomír Sedlář 2930a1cc54
Retry 401 error from CTS
This could be a transient error caused by kerberos server instability.

JIRA: RHELCMP-11251
Signed-off-by: Lubomír Sedlář <lsedlar@redhat.com>
(cherry picked from commit 58036eab84)
2023-11-10 16:50:43 +02:00
Lubomír Sedlář 9c4d3d496d
gather: Better detection of debuginfo in lookaside
If the depsolver wants to include a package that is present in both the
source repo and a lookaside repo, it reliably detects binary packages
present in lookaside, but for debuginfo it's not so reliable.

There is a separate package object for each package in each repo.
Depending on which one is used, debuginfo could be included in the
result or not. This patch fixes that by actually looking if the same
package is present in any lookaside repo.

JIRA: RHELCMP-9373
Signed-off-by: Lubomír Sedlář <lsedlar@redhat.com>
(cherry picked from commit a4476f2570)
2023-11-10 16:50:42 +02:00
Haibo Lin 4637fd6697
Log versions of all installed packages
JIRA: RHELCMP-9493
Signed-off-by: Haibo Lin <hlin@redhat.com>
(cherry picked from commit 8c06b7a3f1)
2023-11-10 16:50:41 +02:00
Lubomír Sedlář 2ff8132eaf
Use authentication for all CTS calls
The update of compose URL relied on environment being set from the
initial import. This got broken when a unique credentials cache started
to be used, and was cleaned up after the import.

JIRA: RHELCMP-11072
Signed-off-by: Lubomír Sedlář <lsedlar@redhat.com>
(cherry picked from commit 64ae81b416)
2023-11-10 16:50:40 +02:00
Lubomír Sedlář f9190d1fd1
Fix black complaints
These are newly detected by black 23.1.0.

Signed-off-by: Lubomír Sedlář <lsedlar@redhat.com>
(cherry picked from commit 826169af7c)
2023-11-10 16:50:38 +02:00
Lubomír Sedlář 80ad0448ec
Add vhd.gz extension to compressed VHD images
JIRA: RHELCMP-11027
Signed-off-by: Lubomír Sedlář <lsedlar@redhat.com>
(cherry picked from commit d97b8bdd33)
2023-11-10 16:50:37 +02:00
Lubomír Sedlář 027380f969
Add vhd-compressed image type
JIRA: RHELCMP-11027
Signed-off-by: Lubomír Sedlář <lsedlar@redhat.com>
(cherry picked from commit 8768b23cbe)
2023-11-10 16:50:36 +02:00
Lubomír Sedlář 41048f60b7
Update to work with latest mock
The `called_once` attribute now raises an exception. Switch to
`assert_called_once` method. Also replace `assertTrue(x.called)` with
`x.assert_called()`.

Signed-off-by: Lubomír Sedlář <lsedlar@redhat.com>
(cherry picked from commit 51628a974d)
2023-11-10 16:50:34 +02:00
Ondrej Nosek 9f8f6a7956
Default bztar format for sdist command
Usage of the 'bztar' format is unchanged, just changing the way
of configuration. The previous method was deprecated.

Signed-off-by: Ondrej Nosek <onosek@redhat.com>
(cherry picked from commit 88327d5784)
2023-11-10 16:50:33 +02:00
Lubomír Sedlář 3d3e4bafdf
- New upstream release 4.5.0
(cherry picked from commit 4dfabb647b (fedora_master))
2023-11-10 16:47:04 +02:00
Lubomír Sedlář 8fe0257e93
Release 4.4.1
(cherry picked from commit 4c604f434a (fedora_master))
2023-11-10 16:46:02 +02:00
Fedora Release Engineering d7b5fd2278
Rebuilt for https://fedoraproject.org/wiki/Fedora_39_Mass_Rebuild
Signed-off-by: Fedora Release Engineering <releng@fedoraproject.org>

(cherry picked from commit bf4f5b6e53 (fedora_master))
2023-11-10 16:44:52 +02:00
Lubomír Sedlář 8b49d4ad61
Backport patch from upstream PR 1690
(cherry picked from commit 2362ef59c5 (fedora_master))
2023-11-10 16:44:19 +02:00
Lubomír Sedlář 57443cd0aa
Backport patch from upstream PR 1690
(cherry picked from commit 9ee6caf117 (fedora_master))
2023-11-10 16:43:47 +02:00
Python Maint 1d146bb8d5
Rebuilt for Python 3.12
(cherry picked from commit 8b8b558fbc (fedora_master))
2023-11-10 16:42:36 +02:00
Lubomír Sedlář 790091b7d7
Release 4.4.0
(cherry picked from commit a6196da315 (fedora_master))
2023-11-10 16:42:10 +02:00
Lubomír Sedlář 28aad3ea40
Rebuild without fedmsg dependencs
(cherry picked from commit d142464ef1 (fedora_master))
2023-11-10 16:41:29 +02:00
Pierre-Yves Chibon 7373b4dbbf
Replace the requirement on fedmsg to one on fedora-messaging
Signed-off-by: Pierre-Yves Chibon <pingou@pingoured.fr>
(cherry picked from commit 802f5fe854)
2023-11-10 16:40:34 +02:00
Lubomír Sedlář 218b11f1b7
Backport patches
(cherry picked from commit 20a5d00961 (fedora_master))
2023-11-10 16:40:33 +02:00
Haibo Lin bfbe9095d2
Release 4.3.8
Signed-off-by: Haibo Lin <hlin@redhat.com>

(cherry picked from commit 3548f55821 (fedora_master))
2023-11-10 16:38:58 +02:00
Lubomír Sedlář eb17182c04
Update license tag to SPDX
(cherry picked from commit f9143f6ea1 (fedora_master))
2023-11-10 16:33:41 +02:00
Stepan Oksanichenko f91f90cf64 - Test empty sub-package 2023-10-26 00:01:45 +03:00
Stepan Oksanichenko 49931082b2 - Test empty sub-package 2023-10-25 23:11:26 +03:00
Stepan Oksanichenko 8ba8609bda - Test empty sub-package 2023-10-25 22:58:28 +03:00
Stepan Oksanichenko 6f495a8133 - Test empty sub-package 2023-10-25 22:55:18 +03:00
Stepan Oksanichenko 2b4bddbfe0 - Test empty sub-package 2023-10-25 22:17:42 +03:00
Stepan Oksanichenko 032cf725de - Bump version
- Changelog
2023-07-25 11:12:03 +03:00
Stepan Oksanichenko 8b11bb81af AL-5220: Investigate why CL9 can't built on the new nebula
- Exclude the packages for using in a build
2023-07-24 18:26:51 +03:00
soksanichenko 114a73f100 - gather-module can find modules through symlinks
- Bump version
- Update changelog
2023-04-15 20:03:27 +03:00
soksanichenko 1c3e5dce5e - CLI option `--label` can be passed through a Pungi config file
- Bump version
- Update changelog
2023-04-13 00:57:39 +03:00
soksanichenko e55abb17f1 - Bump version 2023-04-04 10:12:22 +03:00
soksanichenko e81d78a1d1 - The log message contains a variant's name if Pungi didn't find one or more modules for that 2023-04-04 10:11:59 +03:00
soksanichenko 68915d04f8 - Excluded/included modules/packages will be processed correctly 2023-04-02 22:27:24 +03:00
soksanichenko a25bf72fb8 - Changelog is updated
- Version is bumped
2023-03-31 12:07:22 +03:00
Stepan Oksanichenko 68aee1fa2d Merge pull request 'ALBS-987: Generate i686 and dev repositories with pungi on building new distr. version automatically' (#15) from ALBS-987 into al_master
Reviewed-on: #15
2023-03-31 09:03:39 +00:00
soksanichenko 6592735aec ALBS-987: Generate i686 and dev repositories with pungi on building new distr. version automatically
- Unittests are fixed
2023-03-30 14:05:47 +03:00
soksanichenko 943fd8e77d ALBS-987: Generate i686 and dev repositories with pungi on building new distr. version automatically
- Script `create extra repo` is fixed
- Unittests are fixed
2023-03-30 12:52:51 +03:00
soksanichenko 004fc4382f ALBS-987: Generate i686 and dev repositories with pungi on building new distr. version automatically
- Review comments
2023-03-29 11:40:00 +03:00
soksanichenko 596c5c0b7f ALBS-987: Generate i686 and dev repositories with pungi on building new distr. version automatically
- Refactoring
- Some absent packages are in packages.json now
2023-03-28 12:58:08 +03:00
soksanichenko 141d00e941 ALBS-987: Generate i686 and dev repositories with pungi on building new distr. version automatically
- More info about unsigned packages
2023-03-24 16:39:10 +02:00
soksanichenko 4b64d20826 ALBS-987: Generate i686 and dev repositories with pungi on building new distr. version automatically
- Path.rglob/glob doesn't work with symlinks (it's the bug and reported)
- Refactoring
2023-03-24 12:45:28 +02:00
soksanichenko 0747e967b0 ALBS-987: Generate i686 and dev repositories with pungi on building new distr. version automatically
- Some refactoring
2023-03-23 09:36:52 +02:00
soksanichenko 6d58bc2ed8 ALBS-987: Generate i686 and dev repositories with pungi on building new distr. version automatically
- [Generator of packages.json] Replace using CLI by config.yaml
- [Gather RPMs] os.path is replaced by Path
2023-03-22 15:56:58 +02:00
Stepan Oksanichenko 60a347a4a2 Merge pull request 'ALBS-1030: Generate Devel section in packages.json' (#14) from ALBS-1030 into al_master
Reviewed-on: #14
2023-03-22 10:06:58 +00:00
soksanichenko 53ed7386f3 ALBS-1030: Generate Devel section in packages.json
- Redundant empty lines are removed
2023-03-20 13:56:44 +02:00
soksanichenko ed43f0038e ALBS-1030: Generate Devel section in packages.json
- Style fix
2023-03-20 13:55:06 +02:00
soksanichenko fcc9b4f1ca ALBS-1030: Generate Devel section in packages.json
- Skip verifying an RPM signature if sigkeys are empty
2023-03-20 13:25:45 +02:00
soksanichenko d32c293bca ALBS-1030: Generate Devel section in packages.json
- Some upstream changes to KojiMock parts
2023-03-19 21:11:12 +02:00
soksanichenko f0bd1af999 ALBS-1030: Generate Devel section in packages.json
- Also the tool can combine (remove and add) packages in a variant from different
  sources according to an url's type of source
2023-03-19 18:21:33 +02:00
soksanichenko 1b4747b915 - Changelog is updated
- Version is bumped
- New release 4.3.7-3.alma
2023-03-17 12:02:48 +02:00
Lubomír Sedlář 6aabfc9285 osbuild: test passing of rich repos from configuration
Test that "rich" repositories defined as dicts in the configuration
stay as dicts in the arguments passed to the osbuild phase.

Signed-off-by: Tomáš Hozza <thozza@redhat.com>
(cherry picked from commit 8be0d84f8a)
2023-03-17 11:58:11 +02:00
Tomáš Hozza 9e014fed6a osbuild: support specifying `package_sets` for repos
The `koji-osbuild` plugin supports additional formats for the `repo`
property since v4 [1]. Specifically, a repo can be specified as a
dictionary with `baseurl` key and `package_sets` list containing
specific package set names, that the repository should be used for.

Extend the configuration schema to reflect the plugin change.
Extend the documentation to cover the new repository format.
Extend an existing unit test to specify additional repository using the
added format.

[1] https://github.com/osbuild/koji-osbuild/pull/82

Signed-off-by: Tomáš Hozza <thozza@redhat.com>
(cherry picked from commit 8f0906be53)
2023-03-17 11:58:11 +02:00
Tomáš Hozza 7ccb1d4849 osbuild: don't use `util.get_repo_urls()`
Don't use `util.get_repo_urls()` to resolve provided repositories, but
implement osbuild-specific variant of the function named
`_get_repo_urls(). The reason is that the function from `utils`
transforms repositories defined as dicts to strings, which is
undesired for osbuild. The requirement for osbuild is to preserve the
dict as is, just to resolve the string in `baseurl` to the actual
repository URL.

Add a unit test covering the newly added function. It is inspired by a
similar test from `test_util.py`.

Signed-off-by: Tomáš Hozza <thozza@redhat.com>
(cherry picked from commit e3072c3d5f)
2023-03-17 11:58:11 +02:00
Tomáš Hozza abec28256d osbuild: update schema and config documentation
The `koji-osbuild` Hub schema has been relaxed a bit in the latest
release (v11). Adjust the schema in Pungi to reflect changes in
`koji-osbuild`.

For more information on the changes in `koji-osbuild`, see:
https://github.com/osbuild/koji-osbuild/pull/108

Signed-off-by: Tomáš Hozza <thozza@redhat.com>
(cherry picked from commit ef6d40dce4)
2023-03-17 11:58:11 +02:00
Lubomír Sedlář 46216b4f17 Speed up tests by 30 seconds
The retry test for CTS doesn't actually need to wait. Let's mock the
sleep function.

Signed-off-by: Lubomír Sedlář <lsedlar@redhat.com>
(cherry picked from commit df6664098d)
2023-03-17 11:58:11 +02:00
Lubomír Sedlář 02b3adbaeb Stop sending compose paths to CTS
The tracking service will reject it as it's not an HTTP URL. Let's not
even try.

Signed-off-by: Lubomír Sedlář <lsedlar@redhat.com>
(cherry picked from commit 147df93f75)
2023-03-17 11:58:11 +02:00
Lubomír Sedlář d17e578645 Report errors from CTS
If the service returns a status code indicating a user error, report
that and do not retry.

Signed-off-by: Lubomír Sedlář <lsedlar@redhat.com>
(cherry picked from commit dd8c1002d4)
2023-03-17 11:58:11 +02:00
Lubomír Sedlář 6c1c9d9efd createiso: Create Joliet tree with xorriso
This structure is important for isoinfo -J, which is in turn called by
virt-install.

This can be tested by using a bootable ISO by modifying it with a dummy
additional file and preserving boot records:

    $ xorriso -indev netinst.iso -outdev test.iso -boot_image any replay -map setup.py setup.py -end
    ...
    $ isoinfo -J -i test.iso
    isoinfo: Unable to find Joliet SVD
    $ rm test.iso
    $ xorriso -indev netinst.iso -outdev test.iso -joliet on -boot_image any replay -map setup.py setup.py -end
    ...
    $ isoinfo -J -i test.iso
    $

Fixes: https://bugzilla.redhat.com/show_bug.cgi?id=2144105
Signed-off-by: Lubomír Sedlář <lsedlar@redhat.com>
(cherry picked from commit 12e3a46390)
2023-03-17 11:58:04 +02:00
Stepan Oksanichenko 8dd7d8326f Merge pull request 'ALBS-1040: Investigate why Pungi doesn't put modules packages into the final repos' (#13) from ALBS-1040 into al_master
Reviewed-on: #13
2023-03-16 11:52:02 +00:00
soksanichenko d7b173cae5 ALBS-1040: Investigate why Pungi doesn't put modules packages into the final repos
- The unitttest is fixed
2023-03-14 18:43:14 +02:00
soksanichenko fa4640f03e ALBS-1040: Investigate why Pungi doesn't put modules packages into the final repos
- Refactoring
- KojiMock extracts all modules which are suitable for the variant's arches
2023-03-14 18:25:21 +02:00
Stepan Oksanichenko d66eb0dea8 Merge pull request 'ALBS-1032: Generate i686 section for all variants in packages.json' (#12) from ALBS-1032 into al_master
Reviewed-on: #12
2023-03-14 16:21:41 +00:00
soksanichenko d56227ab4a ALBS-1032: Generate i686 section for all variants in packages.json
- Remove old non-necessary methods
- Some fixes to arch code
2023-03-09 12:32:11 +02:00
soksanichenko 12433157dd - changelog 2022-11-12 00:04:44 +02:00
soksanichenko 623955cb1f - python3-distro as dependency 2022-11-11 19:21:37 +02:00
soksanichenko 4e0d2d14c9 - Unify branch for both RHEL versions 2022-11-11 16:31:43 +02:00
soksanichenko b61e59d676 - Use unittest.mock instead external mock 2022-11-11 15:32:00 +02:00
soksanichenko eb35d7baac - Unify branch for both RHEL versions 2022-11-11 01:38:14 +02:00
soksanichenko 54209f3643 ALBS-732 2022-11-09 21:42:13 +02:00
soksanichenko 80c4536eaa ALBS-732 2022-11-09 21:27:51 +02:00
soksanichenko 9bb5550d36 ALBS-732 2022-11-09 21:01:30 +02:00
soksanichenko 364ed6c3af - kojimock is added to pungi.phases.gather._make_lookaside_repo#prefixes
- unittests are fixed
2022-11-09 20:56:56 +02:00
soksanichenko 0b965096ee - PkgsetSourceKojiMock is added to ALL_SOURCES 2022-11-09 18:18:12 +02:00
soksanichenko d914626d92 - "kojimock" is valid value for option "pkgset_source" 2022-11-09 17:59:50 +02:00
soksanichenko 32215d955a - fedmsg is removed as not needed 2022-11-09 12:38:34 +02:00
soksanichenko d711f8a2d6 - fedmsg is removed as not needed 2022-11-09 09:06:09 +02:00
soksanichenko bd9d800b52 - Fix spec 2022-11-08 17:11:21 +02:00
soksanichenko e03648589d - Fix spec 2022-11-08 17:09:03 +02:00
soksanichenko b5fe2e8129 - Fix spec 2022-11-08 17:06:36 +02:00
103 changed files with 2696 additions and 3293 deletions

View File

@ -2,6 +2,7 @@ include AUTHORS
include COPYING
include GPL
include pungi.spec
include setup.cfg
include tox.ini
include share/*
include share/multilib/*

View File

@ -0,0 +1,2 @@
# Clean up pungi cache
d /var/cache/pungi/createrepo_c/ - - - 30d

View File

@ -18,12 +18,12 @@ import os
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#sys.path.insert(0, os.path.abspath('.'))
# sys.path.insert(0, os.path.abspath('.'))
# -- General configuration ------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#needs_sphinx = '1.0'
# needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
@ -31,207 +31,201 @@ import os
extensions = []
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
templates_path = ["_templates"]
# The suffix of source filenames.
source_suffix = '.rst'
source_suffix = ".rst"
# The encoding of source files.
#source_encoding = 'utf-8-sig'
# source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
master_doc = "index"
# General information about the project.
project = u'Pungi'
copyright = u'2016, Red Hat, Inc.'
project = "Pungi"
copyright = "2016, Red Hat, Inc."
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = '4.3'
version = "4.5"
# The full version, including alpha/beta/rc tags.
release = '4.3.6'
release = "4.5.0"
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#language = None
# language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#today = ''
# today = ''
# Else, today_fmt is used as the format for a strftime call.
#today_fmt = '%B %d, %Y'
# today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = ['_build']
exclude_patterns = ["_build"]
# The reST default role (used for this markup: `text`) to use for all
# documents.
#default_role = None
# default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
#add_function_parentheses = True
# add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#add_module_names = True
# add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#show_authors = False
# show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
pygments_style = "sphinx"
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
# modindex_common_prefix = []
# If true, keep warnings as "system message" paragraphs in the built documents.
#keep_warnings = False
# keep_warnings = False
# -- Options for HTML output ----------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = 'default'
html_theme = "default"
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#html_theme_options = {}
# html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
#html_theme_path = []
# html_theme_path = []
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
#html_title = None
# html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
#html_short_title = None
# html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#html_logo = None
# html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#html_favicon = None
# html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
html_static_path = ["_static"]
# Add any extra paths that contain custom files (such as robots.txt or
# .htaccess) here, relative to this directory. These files are copied
# directly to the root of the documentation.
#html_extra_path = []
# html_extra_path = []
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
#html_last_updated_fmt = '%b %d, %Y'
# html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#html_use_smartypants = True
# html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#html_sidebars = {}
# html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#html_additional_pages = {}
# html_additional_pages = {}
# If false, no module index is generated.
#html_domain_indices = True
# html_domain_indices = True
# If false, no index is generated.
#html_use_index = True
# html_use_index = True
# If true, the index is split into individual pages for each letter.
#html_split_index = False
# html_split_index = False
# If true, links to the reST sources are added to the pages.
#html_show_sourcelink = True
# html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
#html_show_sphinx = True
# html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
#html_show_copyright = True
# html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#html_use_opensearch = ''
# html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = None
# html_file_suffix = None
# Output file base name for HTML help builder.
htmlhelp_basename = 'Pungidoc'
htmlhelp_basename = "Pungidoc"
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
('index', 'Pungi.tex', u'Pungi Documentation',
u'Daniel Mach', 'manual'),
("index", "Pungi.tex", "Pungi Documentation", "Daniel Mach", "manual"),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#latex_logo = None
# latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#latex_use_parts = False
# latex_use_parts = False
# If true, show page references after internal links.
#latex_show_pagerefs = False
# latex_show_pagerefs = False
# If true, show URL addresses after external links.
#latex_show_urls = False
# latex_show_urls = False
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# latex_appendices = []
# If false, no module index is generated.
#latex_domain_indices = True
# latex_domain_indices = True
# -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
('index', 'pungi', u'Pungi Documentation',
[u'Daniel Mach'], 1)
]
man_pages = [("index", "pungi", "Pungi Documentation", ["Daniel Mach"], 1)]
# If true, show URL addresses after external links.
#man_show_urls = False
# man_show_urls = False
# -- Options for Texinfo output -------------------------------------------
@ -240,19 +234,25 @@ man_pages = [
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
('index', 'Pungi', u'Pungi Documentation',
u'Daniel Mach', 'Pungi', 'One line description of project.',
'Miscellaneous'),
(
"index",
"Pungi",
"Pungi Documentation",
"Daniel Mach",
"Pungi",
"One line description of project.",
"Miscellaneous",
),
]
# Documents to append as an appendix to all manuals.
#texinfo_appendices = []
# texinfo_appendices = []
# If false, no module index is generated.
#texinfo_domain_indices = True
# texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
#texinfo_show_urls = 'footnote'
# texinfo_show_urls = 'footnote'
# If true, do not generate a @detailmenu in the "Top" node's menu.
#texinfo_no_detailmenu = False
# texinfo_no_detailmenu = False

View File

@ -194,6 +194,17 @@ Options
Tracking Service Kerberos authentication. If not defined, the default
Kerberos principal is used.
**cts_oidc_token_url**
(*str*) -- URL to the OIDC token endpoint.
For example ``https://oidc.example.com/openid-connect/token``.
This option can be overridden by the environment variable ``CTS_OIDC_TOKEN_URL``.
**cts_oidc_client_id*
(*str*) -- OIDC client ID.
This option can be overridden by the environment variable ``CTS_OIDC_CLIENT_ID``.
Note that environment variable ``CTS_OIDC_CLIENT_SECRET`` must be configured with
corresponding client secret to authenticate to CTS via OIDC.
**compose_type**
(*str*) -- Allows to set default compose type. Type set via a command-line
option overwrites this.
@ -581,6 +592,16 @@ Options
with everything. Set this option to ``False`` to ignore ``noarch`` in
``ExclusiveArch`` and always consider only binary architectures.
**pkgset_inherit_exclusive_arch_to_noarch** = True
(*bool*) -- When set to ``True``, the value of ``ExclusiveArch`` or
``ExcludeArch`` will be copied from source rpm to all its noarch packages.
That will than limit which architectures the noarch packages can be
included in.
By setting this option to ``False`` this step is skipped, and noarch
packages will by default land in all architectures. They can still be
excluded by listing them in a relevant section of ``filter_packages``.
**pkgset_allow_reuse** = True
(*bool*) -- When set to ``True``, *Pungi* will try to reuse pkgset data
from the old composes specified by ``--old-composes``. When enabled, this
@ -920,6 +941,10 @@ Options
comps file can not be found in the package set. When disabled (the
default), such cases are still reported as warnings in the log.
With ``dnf`` gather backend, this option will abort the compose on any
missing package no matter if it's listed in comps, ``additional_packages``
or prepopulate file.
**gather_source_mapping**
(*str*) -- JSON mapping with initial packages for the compose. The value
should be a path to JSON file with following mapping: ``{variant: {arch:
@ -1607,8 +1632,23 @@ OSBuild Composer for building images
* ``release`` -- release part of the final NVR. If neither this option nor
the global ``osbuild_release`` is set, Koji will automatically generate a
value.
* ``repo`` -- a list of repository URLs from which to consume packages for
* ``repo`` -- a list of repositories from which to consume packages for
building the image. By default only the variant repository is used.
The list items may use one of the following formats:
* String with just the repository URL.
* Dictionary with the following keys:
* ``baseurl`` -- URL of the repository.
* ``package_sets`` -- a list of package set names to use for this
repository. Package sets are an internal concept of Image Builder
and are used in image definitions. If specified, the repository is
used by Image Builder only for the pipeline with the same name.
For example, specifying the ``build`` package set name will make
the repository to be used only for the build environment in which
the image will be built. (optional)
* ``arches`` -- list of architectures for which to build the image. By
default, the variant arches are used. This option can only restrict it,
not add a new one.
@ -1641,13 +1681,13 @@ OSBuild Composer for building images
* ``tenant_id`` -- Azure tenant ID to upload the image to
* ``subscription_id`` -- Azure subscription ID to upload the image to
* ``resource_group`` -- Azure resource group to upload the image to
* ``location`` -- Azure location to upload the image to
* ``location`` -- Azure location of the resource group (optional)
* ``image_name`` -- Image name of the uploaded Azure image (optional)
* **GCP upload options** -- upload to Google Cloud Platform.
* ``region`` -- GCP region to upload the image to
* ``bucket`` -- GCP bucket to upload the image to
* ``bucket`` -- GCP bucket to upload the image to (optional)
* ``share_with_accounts`` -- list of GCP accounts to share the image
with
* ``image_name`` -- Image name of the uploaded GCP image (optional)
@ -1764,6 +1804,8 @@ repository with a new commit.
* ``tag_ref`` -- (*bool*, default ``True``) If set to ``False``, a git
reference will not be created.
* ``ostree_ref`` -- (*str*) To override value ``ref`` from ``treefile``.
* ``runroot_packages`` -- (*list*) A list of additional package names to be
installed in the runroot environment in Koji.
Example config
--------------

View File

@ -19,7 +19,7 @@ Contents:
scm_support
messaging
gathering
koji
comps
contributing
testing
multi_compose

105
doc/koji.rst Normal file
View File

@ -0,0 +1,105 @@
======================
Getting data from koji
======================
When Pungi is configured to get packages from a Koji tag, it somehow needs to
access the actual RPM files.
Historically, this required the storage used by Koji to be directly available
on the host where Pungi was running. This was usually achieved by using NFS for
the Koji volume, and mounting it on the compose host.
The compose could be created directly on the same volume. In such case the
packages would be hardlinked, significantly reducing space consumption.
The compose could also be created on a different storage, in which case the
packages would either need to be copied over or symlinked. Using symlinks
requires that anything that accesses the compose (e.g. a download server) would
also need to mount the Koji volume in the same location.
There is also a risk with symlinks that the package in Koji can change (due to
being resigned for example), which would invalidate composes linking to it.
Using Koji without direct mount
===============================
It is possible now to run a compose from a Koji tag without direct access to
Koji storage.
Pungi can download the packages over HTTP protocol, store them in a local
cache, and consume them from there.
The local cache has similar structure to what is on the Koji volume.
When Pungi needs some package, it has a path on Koji volume. It will replace
the ``topdir`` with the cache location. If such file exists, it will be used.
If it doesn't exist, it will be downloaded from Koji (by replacing the
``topdir`` with ``topurl``).
::
Koji path /mnt/koji/packages/foo/1/1.fc38/data/signed/abcdef/noarch/foo-1-1.fc38.noarch.rpm
Koji URL https://kojipkgs.fedoraproject.org/packages/foo/1/1.fc38/data/signed/abcdef/noarch/foo-1-1.fc38.noarch.rpm
Local path /mnt/compose/cache/packages/foo/1/1.fc38/data/signed/abcdef/noarch/foo-1-1.fc38.noarch.rpm
The packages can be hardlinked from this cache directory.
Cleanup
-------
While the approach above allows each RPM to be downloaded only once, it will
eventually result in the Koji volume being mirrored locally. Most of the
packages will however no longer be needed.
There is a script ``pungi-cache-cleanup`` that can help with that. It can find
and remove files from the cache that are no longer needed.
A file is no longer needed if it has a single link (meaning it is only in the
cache, not in any compose), and it has mtime older than a given threshold.
It doesn't make sense to delete files that are hardlinked in an existing
compose as it would not save any space anyway.
The mtime check is meant to preserve files that are downloaded but not actually
used in a compose, like a subpackage that is not included in any variant. Every
time its existence in the local cache is checked, the mtime is updated.
Race conditions?
----------------
It should be safe to have multiple compose hosts share the same storage volume
for generated composes and local cache.
If a cache file is accessed and it exists, there's no risk of race condition.
If two composes need the same file at the same time and it is not present yet,
one of them will take a lock on it and start downloading. The other will wait
until the download is finished.
The lock is only valid for a set amount of time (5 minutes) to avoid issues
where the downloading process is killed in a way that blocks it from releasing
the lock.
If the file is large and network slow, the limit may not be enough finish
downloading. In that case the second process will steal the lock while the
first process is still downloading. This will result in the same file being
downloaded twice.
When the first process finishes the download, it will put the file into the
local cache location. When the second process finishes, it will atomically
replace it, but since it's the same file it will be the same file.
If the first compose already managed to hardlink the file before it gets
replaced, there will be two copies of the file present locally.
Integrity checking
------------------
There is minimal integrity checking. RPM packages belonging to real builds will
be check to match the checksum provided by Koji hub.
There is no checking for scratch builds or any images.

View File

@ -1,107 +0,0 @@
.. _multi_compose:
Managing compose from multiple parts
====================================
There may be cases where it makes sense to split a big compose into separate
parts, but create a compose output that links all output into one familiar
structure.
The `pungi-orchestrate` tools allows that.
It works with an INI-style configuration file. The ``[general]`` section
contains information about identity of the main compose. Other sections define
individual parts.
The parts are scheduled to run in parallel, with the minimal amount of
serialization. The final compose directory will contain hard-links to the
files.
General settings
----------------
**target**
Path to directory where the final compose should be created.
**compose_type**
Type of compose to make.
**release_name**
Name of the product for the final compose.
**release_short**
Short name of the product for the final compose.
**release_version**
Version of the product for the final compose.
**release_type**
Type of the product for the final compose.
**extra_args**
Additional arguments that will be passed to the child Pungi processes.
**koji_profile**
If specified, a current event will be retrieved from the Koji instance and
used for all parts.
**kerberos**
If set to yes, a kerberos ticket will be automatically created at the start.
Set keytab and principal as well.
**kerberos_keytab**
Path to keytab file used to create the kerberos ticket.
**kerberos_principal**
Kerberos principal for the ticket
**pre_compose_script**
Commands to execute before first part is started. Can contain multiple
commands on separate lines.
**post_compose_script**
Commands to execute after the last part finishes and final status is
updated. Can contain multiple commands on separate lines. ::
post_compose_script =
compose-latest-symlink $COMPOSE_PATH
custom-post-compose-script.sh
Multiple environment variables are defined for the scripts:
* ``COMPOSE_PATH``
* ``COMPOSE_ID``
* ``COMPOSE_DATE``
* ``COMPOSE_TYPE``
* ``COMPOSE_RESPIN``
* ``COMPOSE_LABEL``
* ``RELEASE_ID``
* ``RELEASE_NAME``
* ``RELEASE_SHORT``
* ``RELEASE_VERSION``
* ``RELEASE_TYPE``
* ``RELEASE_IS_LAYERED`` ``YES`` for layered products, empty otherwise
* ``BASE_PRODUCT_NAME`` only set for layered products
* ``BASE_PRODUCT_SHORT`` only set for layered products
* ``BASE_PRODUCT_VERSION`` only set for layered products
* ``BASE_PRODUCT_TYPE`` only set for layered products
**notification_script**
Executable name (or path to a script) that will be used to send a message
once the compose is finished. In order for a valid URL to be included in the
message, at least one part must configure path translation that would apply
to location of main compose.
Only two messages will be sent, one for start and one for finish (either
successful or not).
Partial compose settings
------------------------
Each part should have a separate section in the config file.
It can specify these options:
**config**
Path to configuration file that describes this part. If relative, it is
resolved relative to the file with parts configuration.
**just_phase**, **skip_phase**
Customize which phases should run for this part.
**depends_on**
A comma separated list of other parts that must be finished before this part
starts.
**failable**
A boolean toggle to mark a part as failable. A failure in such part will
mark the final compose as incomplete, but still successful.

View File

@ -41,6 +41,14 @@ which can contain following keys.
* ``command`` -- defines a shell command to run after Git clone to generate the
needed file (for example to run ``make``). Only supported in Git backend.
* ``options`` -- a dictionary of additional configuration options. These are
specific to different backends.
Currently supported values for Git:
* ``credential_helper`` -- path to a credential helper used to supply
username/password for remotes that require authentication.
Koji examples
-------------

View File

@ -1,27 +1,24 @@
%{?python_enable_dependency_generator}
Name: pungi
Version: 4.3.6
Release: 2%{?dist}.alma
Version: 4.5.0
Release: 3%{?dist}.alma
Summary: Distribution compose tool
License: GPLv2
License: GPL-2.0-only
URL: https://pagure.io/pungi
Source0: %{name}-%{version}.tar.bz2
BuildRequires: make
BuildRequires: python3-nose
BuildRequires: python3-pytest
BuildRequires: python3-mock
BuildRequires: python3-pyfakefs
BuildRequires: python3-ddt
# replaced by unittest.mock
# BuildRequires: python3-mock
BuildRequires: python3-devel
BuildRequires: python3-setuptools
BuildRequires: python3-productmd >= 1.33
BuildRequires: python3-kobo-rpmlib >= 0.18.0
BuildRequires: createrepo_c >= 0.20.1
BuildRequires: python3-lxml
BuildRequires: python3-ddt
BuildRequires: python3-kickstart
BuildRequires: python3-rpm
BuildRequires: python3-dnf
@ -39,17 +36,21 @@ BuildRequires: python3-gobject
BuildRequires: python3-createrepo_c >= 0.20.1
BuildRequires: python3-dogpile-cache
BuildRequires: python3-parameterized
BuildRequires: python3-flufl-lock
BuildRequires: python3-ddt
BuildRequires: python3-distro
BuildRequires: python3-gobject-base
BuildRequires: python3-pgpy
BuildRequires: python3-pyfakefs
%if %{rhel} == 8
BuildRequires: python3-dataclasses
%endif
#deps for doc building
BuildRequires: python3-sphinx
Requires: python3-kobo-rpmlib >= 0.18.0
Requires: python3-productmd >= 1.33
Requires: python3-kickstart
Requires: python3-requests
Requires: python3-dataclasses
Requires: createrepo_c >= 0.20.1
Requires: koji >= 1.10.1-13
Requires: python3-koji-cli-plugins
@ -64,9 +65,17 @@ Requires: python3-libmodulemd >= 2.8.0
Requires: python3-gobject
Requires: python3-createrepo_c >= 0.20.1
Requires: python3-PyYAML
Requires: python3-productmd >= 1.28R
Requires: python3-gobject-base
Requires: python3-productmd >= 1.28
Requires: python3-flufl-lock
Requires: python3-productmd >= 1.33
Requires: lorax
Requires: python3-distro
Requires: python3-gobject-base
Requires: python3-pgpy
Requires: python3-requests
%if %{rhel} == 8
Requires: python3-dataclasses
%endif
# This package is not available on i686, hence we cannot require it
# See https://bugzilla.redhat.com/show_bug.cgi?id=1743421
@ -82,7 +91,7 @@ A tool to create anaconda based installation trees/isos of a set of rpms.
%package utils
Summary: Utilities for working with finished composes
Requires: pungi = %{version}-%{release}
Requires: python3-fedmsg
Requires: python3-fedora-messaging
%description utils
These utilities work with finished composes produced by Pungi. They can be used
@ -91,8 +100,8 @@ notification to Fedora Message Bus.
%package -n python3-%{name}
Summary: Python 3 libraries for pungi
Requires: python3-attrs
Requires: fus
Requires: python3-attrs
%description -n python3-%{name}
Python library with code for Pungi. This is not a public library and there are
@ -118,8 +127,6 @@ gzip _build/man/pungi.1
rm %{buildroot}%{_bindir}/pungi
rm %{buildroot}%{_bindir}/%{name}-fedmsg-notification
%check
%pytest
@ -137,8 +144,9 @@ rm %{buildroot}%{_bindir}/%{name}-fedmsg-notification
%{_bindir}/%{name}-make-ostree
%{_mandir}/man1/pungi.1.gz
%{_datadir}/pungi
/var/cache/pungi
%dir %attr(1777, root, root) /var/cache/pungi/createrepo_c
%{_localstatedir}/cache/pungi
%dir %attr(1777, root, root) %{_localstatedir}/cache/pungi/createrepo_c
%{_tmpfilesdir}/pungi-clean-cache.conf
%files -n python3-%{name}
%{python3_sitelib}/%{name}
@ -151,13 +159,132 @@ rm %{buildroot}%{_bindir}/%{name}-fedmsg-notification
%{_bindir}/%{name}-config-validate
%{_bindir}/%{name}-fedmsg-notification
%{_bindir}/%{name}-notification-report-progress
%{_bindir}/%{name}-orchestrate
%{_bindir}/%{name}-patch-iso
%{_bindir}/%{name}-compare-depsolving
%{_bindir}/%{name}-wait-for-signed-ostree-handler
%{_bindir}/%{name}-cache-cleanup
%changelog
* Mon Nov 21 2023 Stepan Oksanichenko <soksanichenko@almalinux.org> - 4.5.0-3
- Method `get_remote_file_content` is object's method now
* Wed Nov 15 2023 Stepan Oksanichenko <soksanichenko@almalinux.org> - 4.5.0-2
- Return empty list if a repo doesn't contain any module
* Thu Aug 31 2023 Lubomír Sedlář <lsedlar@redhat.com> - 4.5.0-1
- kojiwrapper: Stop being smart about local access (lsedlar)
- Fix unittest errors (ounsal)
- Add integrity checking for builds (lsedlar)
- Add script for cleaning up the cache (lsedlar)
- Add ability to download images (lsedlar)
- Add support for not having koji volume mounted locally (lsedlar)
- Remove repository cloning multiple times (abisoi)
- Support require_all_comps_packages on DNF backend (lsedlar)
- Fix new warnings from flake8 (lsedlar)
* Tue Jul 25 2023 Stepan Oksanichenko <soksanichenko@cloudlinux.com> - 4.3.7-8
- Option `excluded-packages` for script `pungi-gather-rpms`
* Tue Jul 25 2023 Lubomír Sedlář <lsedlar@redhat.com> - 4.4.1-1
- ostree: Add configuration for custom runroot packages (lsedlar)
- pkgset: Emit better error for missing modulemd file (lsedlar)
- Add support for git-credential-helper (lsedlar)
- Support OIDC Client Credentials authentication to CTS (hlin)
* Fri Jul 21 2023 Fedora Release Engineering <releng@fedoraproject.org> - 4.4.0-4
- Rebuilt for https://fedoraproject.org/wiki/Fedora_39_Mass_Rebuild
* Wed Jul 19 2023 Lubomír Sedlář <lsedlar@redhat.com> - 4.4.0-3
- Backport ostree runroot package additions
* Wed Jul 19 2023 Lubomír Sedlář <lsedlar@redhat.com> - 4.4.0-2
- Backport ostree runroot package additions
* Mon Jun 19 2023 Python Maint <python-maint@redhat.com> - 4.4.0-2
- Rebuilt for Python 3.12
* Wed Jun 07 2023 Lubomír Sedlář <lsedlar@redhat.com> - 4.4.0-1
- gather-dnf: Run latest() later (lsedlar)
- iso: Support joliet long names (lsedlar)
- Drop pungi-orchestrator code (lsedlar)
- isos: Ensure proper file ownership and permissions (lsedlar)
- gather: Always get latest packages (lsedlar)
- Add back compatibility with jsonschema <3.0.0 (lsedlar)
- Remove useless debug message (lsedlar)
- Remove fedmsg from requirements (lsedlar)
- gather: Support dotarch in DNF backend (lsedlar)
- Fix compatibility with createrepo_c 0.21.1 (lsedlar)
- comps: Apply arch filtering to environment/optionlist (lsedlar)
- Add config file for cleaning up cache files (hlin)
* Wed May 17 2023 Lubomír Sedlář <lsedlar@redhat.com> - 4.3.8-3
- Rebuild without fedmsg dependency
* Wed May 03 2023 Lubomír Sedlář <lsedlar@redhat.com> - 4.3.8-1
- Set priority for Fedora messages
* Thu Apr 13 2023 Stepan Oksanichenko <soksanichenko@cloudlinux.com> - 4.3.7-7
- gather-module can find modules through symlinks
* Thu Apr 13 2023 Stepan Oksanichenko <soksanichenko@cloudlinux.com> - 4.3.7-6
- CLI option `--label` can be passed through a Pungi config file
* Fri Mar 31 2023 Stepan Oksanichenko <soksanichenko@cloudlinux.com> - 4.3.7-4
- ALBS-1030: Generate Devel section in packages.json
- Also the tool can combine (remove and add) packages in a variant from different sources according to an url's type of source
- Some upstream changes to KojiMock part
- Skip verifying an RPM signature if sigkeys are empty
- ALBS-987: Generate i686 and dev repositories with pungi on building new distr. version automatically
- [Generator of packages.json] Replace using CLI by config.yaml
- [Gather RPMs] os.path is replaced by Pat
* Thu Mar 30 2023 Haibo Lin <hlin@redhat.com> - 4.3.8-1
- createiso: Update possibly changed file on DVD (lsedlar)
- pkgset: Stop reuse if configuration changed (lsedlar)
- Allow disabling inheriting ExcludeArch to noarch packages (lsedlar)
- pkgset: Support extra builds with no tags (lsedlar)
- buildinstall: Avoid pointlessly tweaking the boot images (lsedlar)
- Prevent to reuse if unsigned packages are allowed (hlin)
- Pass parent id/respin id to CTS (lsedlar)
- Exclude existing files in boot.iso (hlin)
- image-build/osbuild: Pull ISOs into the compose (lsedlar)
- Retry 401 error from CTS (lsedlar)
- gather: Better detection of debuginfo in lookaside (lsedlar)
- Log versions of all installed packages (hlin)
- Use authentication for all CTS calls (lsedlar)
- Fix black complaints (lsedlar)
- Add vhd.gz extension to compressed VHD images (lsedlar)
- Add vhd-compressed image type (lsedlar)
- Update to work with latest mock (lsedlar)
- Default bztar format for sdist command (onosek)
* Fri Mar 17 2023 Stepan Oksanichenko <soksanichenko@cloudlinux.com> - 4.3.7-3
- ALBS-987: Generate i686 repositories with pungi on building new distr. version automatically
- KojiMock extracts all modules which are suitable for the variant's arches
- An old code is removed or refactored
* Fri Jan 20 2023 Fedora Release Engineering <releng@fedoraproject.org> - 4.3.7-2
- Rebuilt for https://fedoraproject.org/wiki/Fedora_38_Mass_Rebuild
* Fri Dec 09 2022 Ondřej Nosek <onosek@redhat.com> - 4.3.7-1
- osbuild: test passing of rich repos from configuration (lsedlar)
- osbuild: support specifying `package_sets` for repos (thozza)
- osbuild: don't use `util.get_repo_urls()` (thozza)
- osbuild: update schema and config documentation (thozza)
- Speed up tests by 30 seconds (lsedlar)
- Stop sending compose paths to CTS (lsedlar)
- Report errors from CTS (lsedlar)
- createiso: Create Joliet tree with xorriso (lsedlar)
- init: Filter comps for modular variants with tags (lsedlar)
- Retry failed cts requests (hlin)
- Ignore existing kerberos ticket for CTS auth (lsedlar)
- osbuild: support specifying upload_options (thozza)
- osbuild: accept only a single image type in the configuration (thozza)
- Add Jenkinsfile for CI (hlin)
- profiler: Flush stdout before printing (lsedlar)
* Sat Nov 12 2022 Stepan Oksanichenko <soksanichenko@cloudlinux.com> - 4.3.6-3
- AlmaLinux version. Updates from upstream
* Mon Nov 07 2022 Lubomír Sedlář <lsedlar@redhat.com> - 4.3.6-2
- Stop including comps in modular repos

View File

@ -227,9 +227,19 @@ def validate(config, offline=False, schema=None):
DefaultValidator = _extend_with_default_and_alias(
jsonschema.Draft4Validator, offline=offline
)
validator = DefaultValidator(
schema,
)
if hasattr(jsonschema.Draft4Validator, "TYPE_CHECKER"):
# jsonschema >= 3.0 has new interface for checking types
validator = DefaultValidator(schema)
else:
validator = DefaultValidator(
schema,
{
"array": (tuple, list),
"regex": six.string_types,
"url": six.string_types,
},
)
errors = []
warnings = []
for error in validator.iter_errors(config):
@ -377,6 +387,7 @@ def _extend_with_default_and_alias(validator_class, offline=False):
instance[property]["branch"] = resolver(
instance[property]["repo"],
instance[property].get("branch") or "HEAD",
instance[property].get("options"),
)
for error in _hook_errors(properties, instance, schema):
@ -444,13 +455,20 @@ def _extend_with_default_and_alias(validator_class, offline=False):
context=all_errors,
)
def is_array(checker, instance):
return isinstance(instance, (tuple, list))
kwargs = {}
if hasattr(validator_class, "TYPE_CHECKER"):
# jsonschema >= 3
def is_array(checker, instance):
return isinstance(instance, (tuple, list))
def is_string_type(checker, instance):
return isinstance(instance, six.string_types)
def is_string_type(checker, instance):
return isinstance(instance, six.string_types)
validator = jsonschema.validators.extend(
kwargs["type_checker"] = validator_class.TYPE_CHECKER.redefine_many(
{"array": is_array, "regex": is_string_type, "url": is_string_type}
)
return jsonschema.validators.extend(
validator_class,
{
"properties": properties_validator,
@ -460,13 +478,8 @@ def _extend_with_default_and_alias(validator_class, offline=False):
"additionalProperties": _validate_additional_properties,
"anyOf": _validate_any_of,
},
**kwargs
)
validator.DEFAULT_TYPES.update({
"array": (list, tuple),
"regex": six.string_types,
"url": six.string_types,
})
return validator
class ConfigDeprecation(jsonschema.exceptions.ValidationError):
@ -508,6 +521,13 @@ def make_schema():
"file": {"type": "string"},
"dir": {"type": "string"},
"command": {"type": "string"},
"options": {
"type": "object",
"properties": {
"credential_helper": {"type": "string"},
},
"additionalProperties": False,
},
},
"additionalProperties": False,
},
@ -589,6 +609,7 @@ def make_schema():
"release_discinfo_description": {"type": "string"},
"treeinfo_version": {"type": "string"},
"compose_type": {"type": "string", "enum": COMPOSE_TYPES},
"label": {"type": "string"},
"base_product_name": {"type": "string"},
"base_product_short": {"type": "string"},
"base_product_version": {"type": "string"},
@ -666,7 +687,11 @@ def make_schema():
"pkgset_allow_reuse": {"type": "boolean", "default": True},
"createiso_allow_reuse": {"type": "boolean", "default": True},
"extraiso_allow_reuse": {"type": "boolean", "default": True},
"pkgset_source": {"type": "string", "enum": ["koji", "repos"]},
"pkgset_source": {"type": "string", "enum": [
"koji",
"repos",
"kojimock",
]},
"createrepo_c": {"type": "boolean", "default": True},
"createrepo_checksum": {
"type": "string",
@ -820,8 +845,11 @@ def make_schema():
"pdc_insecure": {"deprecated": "Koji is queried instead"},
"cts_url": {"type": "string"},
"cts_keytab": {"type": "string"},
"cts_oidc_token_url": {"type": "url"},
"cts_oidc_client_id": {"type": "string"},
"koji_profile": {"type": "string"},
"koji_event": {"type": "number"},
"koji_cache": {"type": "string"},
"pkgset_koji_tag": {"$ref": "#/definitions/strings"},
"pkgset_koji_builds": {"$ref": "#/definitions/strings"},
"pkgset_koji_scratch_tasks": {"$ref": "#/definitions/strings"},
@ -839,6 +867,10 @@ def make_schema():
"type": "boolean",
"default": True,
},
"pkgset_inherit_exclusive_arch_to_noarch": {
"type": "boolean",
"default": True,
},
"pkgset_scratch_modules": {
"type": "object",
"patternProperties": {
@ -1041,6 +1073,9 @@ def make_schema():
"config_branch": {"type": "string"},
"tag_ref": {"type": "boolean"},
"ostree_ref": {"type": "string"},
"runroot_packages": {
"$ref": "#/definitions/list_of_strings",
},
},
"required": [
"treefile",
@ -1197,14 +1232,36 @@ def make_schema():
},
"arches": {"$ref": "#/definitions/list_of_strings"},
"release": {"type": "string"},
"repo": {"$ref": "#/definitions/list_of_strings"},
"repo": {
"type": "array",
"items": {
"oneOf": [
{
"type": "object",
"additionalProperties": False,
"required": ["baseurl"],
"properties": {
"baseurl": {"type": "string"},
"package_sets": {
"type": "array",
"items": {"type": "string"},
},
},
},
{"type": "string"},
]
},
},
"failable": {"$ref": "#/definitions/list_of_strings"},
"subvariant": {"type": "string"},
"ostree_url": {"type": "string"},
"ostree_ref": {"type": "string"},
"ostree_parent": {"type": "string"},
"upload_options": {
"oneOf": [
# this should be really 'oneOf', but the minimal
# required properties in AWSEC2 and GCP options
# overlap.
"anyOf": [
# AWSEC2UploadOptions
{
"type": "object",
@ -1243,7 +1300,6 @@ def make_schema():
"tenant_id",
"subscription_id",
"resource_group",
"location",
],
"properties": {
"tenant_id": {"type": "string"},
@ -1259,7 +1315,7 @@ def make_schema():
{
"type": "object",
"additionalProperties": False,
"required": ["region", "bucket"],
"required": ["region"],
"properties": {
"region": {"type": "string"},
"bucket": {"type": "string"},

View File

@ -17,6 +17,7 @@
__all__ = ("Compose",)
import contextlib
import errno
import logging
import os
@ -38,6 +39,7 @@ from dogpile.cache import make_region
from pungi.graph import SimpleAcyclicOrientedGraph
from pungi.wrappers.variants import VariantsXmlParser
from pungi.paths import Paths
from pungi.wrappers.kojiwrapper import KojiDownloadProxy
from pungi.wrappers.scm import get_file_from_scm
from pungi.util import (
makedirs,
@ -57,14 +59,101 @@ except ImportError:
SUPPORTED_MILESTONES = ["RC", "Update", "SecurityFix"]
def is_status_fatal(status_code):
"""Check if status code returned from CTS reports an error that is unlikely
to be fixed by retrying. Generally client errors (4XX) are fatal, with the
exception of 401 Unauthorized which could be caused by transient network
issue between compose host and KDC.
"""
if status_code == 401:
return False
return status_code >= 400 and status_code < 500
@retry(wait_on=RequestException)
def retry_request(method, url, data=None, auth=None):
def retry_request(method, url, data=None, json_data=None, auth=None):
"""
:param str method: Reqest method.
:param str url: Target URL.
:param dict data: form-urlencoded data to send in the body of the request.
:param dict json_data: json data to send in the body of the request.
"""
request_method = getattr(requests, method)
rv = request_method(url, json=data, auth=auth)
rv = request_method(url, data=data, json=json_data, auth=auth)
if is_status_fatal(rv.status_code):
try:
error = rv.json()
except ValueError:
error = rv.text
raise RuntimeError("%s responded with %d: %s" % (url, rv.status_code, error))
rv.raise_for_status()
return rv
class BearerAuth(requests.auth.AuthBase):
def __init__(self, token):
self.token = token
def __call__(self, r):
r.headers["authorization"] = "Bearer " + self.token
return r
@contextlib.contextmanager
def cts_auth(pungi_conf):
"""
:param dict pungi_conf: dict obj of pungi.json config.
"""
auth = None
token = None
cts_keytab = pungi_conf.get("cts_keytab")
cts_oidc_token_url = os.environ.get("CTS_OIDC_TOKEN_URL", "") or pungi_conf.get(
"cts_oidc_token_url"
)
try:
if cts_keytab:
# requests-kerberos cannot accept custom keytab, we need to use
# environment variable for this. But we need to change environment
# only temporarily just for this single requests.post.
# So at first backup the current environment and revert to it
# after the requests call.
from requests_kerberos import HTTPKerberosAuth
auth = HTTPKerberosAuth()
environ_copy = dict(os.environ)
if "$HOSTNAME" in cts_keytab:
cts_keytab = cts_keytab.replace("$HOSTNAME", socket.gethostname())
os.environ["KRB5_CLIENT_KTNAME"] = cts_keytab
os.environ["KRB5CCNAME"] = "DIR:%s" % tempfile.mkdtemp()
elif cts_oidc_token_url:
cts_oidc_client_id = os.environ.get(
"CTS_OIDC_CLIENT_ID", ""
) or pungi_conf.get("cts_oidc_client_id", "")
token = retry_request(
"post",
cts_oidc_token_url,
data={
"grant_type": "client_credentials",
"client_id": cts_oidc_client_id,
"client_secret": os.environ.get("CTS_OIDC_CLIENT_SECRET", ""),
},
).json()["access_token"]
auth = BearerAuth(token)
del token
yield auth
except Exception as e:
# Avoid leaking client secret in trackback
e.show_locals = False
raise e
finally:
if cts_keytab:
shutil.rmtree(os.environ["KRB5CCNAME"].split(":", 1)[1])
os.environ.clear()
os.environ.update(environ_copy)
def get_compose_info(
conf,
compose_type="production",
@ -94,38 +183,19 @@ def get_compose_info(
ci.compose.type = compose_type
ci.compose.date = compose_date or time.strftime("%Y%m%d", time.localtime())
ci.compose.respin = compose_respin or 0
ci.compose.id = ci.create_compose_id()
cts_url = conf.get("cts_url", None)
cts_url = conf.get("cts_url")
if cts_url:
# Requests-kerberos cannot accept custom keytab, we need to use
# environment variable for this. But we need to change environment
# only temporarily just for this single requests.post.
# So at first backup the current environment and revert to it
# after the requests.post call.
cts_keytab = conf.get("cts_keytab", None)
authentication = get_authentication(conf)
if cts_keytab:
environ_copy = dict(os.environ)
if "$HOSTNAME" in cts_keytab:
cts_keytab = cts_keytab.replace("$HOSTNAME", socket.gethostname())
os.environ["KRB5_CLIENT_KTNAME"] = cts_keytab
os.environ["KRB5CCNAME"] = "DIR:%s" % tempfile.mkdtemp()
try:
# Create compose in CTS and get the reserved compose ID.
ci.compose.id = ci.create_compose_id()
url = os.path.join(cts_url, "api/1/composes/")
data = {
"compose_info": json.loads(ci.dumps()),
"parent_compose_ids": parent_compose_ids,
"respin_of": respin_of,
}
rv = retry_request("post", url, data=data, auth=authentication)
finally:
if cts_keytab:
shutil.rmtree(os.environ["KRB5CCNAME"].split(":", 1)[1])
os.environ.clear()
os.environ.update(environ_copy)
# Create compose in CTS and get the reserved compose ID.
url = os.path.join(cts_url, "api/1/composes/")
data = {
"compose_info": json.loads(ci.dumps()),
"parent_compose_ids": parent_compose_ids,
"respin_of": respin_of,
}
with cts_auth(conf) as authentication:
rv = retry_request("post", url, json_data=data, auth=authentication)
# Update local ComposeInfo with received ComposeInfo.
cts_ci = ComposeInfo()
@ -133,22 +203,9 @@ def get_compose_info(
ci.compose.respin = cts_ci.compose.respin
ci.compose.id = cts_ci.compose.id
else:
ci.compose.id = ci.create_compose_id()
return ci
def get_authentication(conf):
authentication = None
cts_keytab = conf.get("cts_keytab", None)
if cts_keytab:
from requests_kerberos import HTTPKerberosAuth
authentication = HTTPKerberosAuth()
return authentication
def write_compose_info(compose_dir, ci):
"""
Write ComposeInfo `ci` to `compose_dir` subdirectories.
@ -162,17 +219,20 @@ def write_compose_info(compose_dir, ci):
def update_compose_url(compose_id, compose_dir, conf):
authentication = get_authentication(conf)
cts_url = conf.get("cts_url", None)
if cts_url:
url = os.path.join(cts_url, "api/1/composes", compose_id)
tp = conf.get("translate_paths", None)
compose_url = translate_path_raw(tp, compose_dir)
if compose_url == compose_dir:
# We do not have a URL, do not attempt the update.
return
data = {
"action": "set_url",
"compose_url": compose_url,
}
return retry_request("patch", url, data=data, auth=authentication)
with cts_auth(conf) as authentication:
return retry_request("patch", url, json_data=data, auth=authentication)
def get_compose_dir(
@ -183,11 +243,19 @@ def get_compose_dir(
compose_respin=None,
compose_label=None,
already_exists_callbacks=None,
parent_compose_ids=None,
respin_of=None,
):
already_exists_callbacks = already_exists_callbacks or []
ci = get_compose_info(
conf, compose_type, compose_date, compose_respin, compose_label
conf,
compose_type,
compose_date,
compose_respin,
compose_label,
parent_compose_ids,
respin_of,
)
cts_url = conf.get("cts_url", None)
@ -342,6 +410,8 @@ class Compose(kobo.log.LoggingBase):
else:
self.cache_region = make_region().configure("dogpile.cache.null")
self.koji_downloader = KojiDownloadProxy.from_config(self.conf, self._logger)
get_compose_info = staticmethod(get_compose_info)
write_compose_info = staticmethod(write_compose_info)
get_compose_dir = staticmethod(get_compose_dir)
@ -637,7 +707,7 @@ class Compose(kobo.log.LoggingBase):
separators=(",", ": "),
)
def traceback(self, detail=None):
def traceback(self, detail=None, show_locals=True):
"""Store an extended traceback. This method should only be called when
handling an exception.
@ -649,7 +719,7 @@ class Compose(kobo.log.LoggingBase):
tb_path = self.paths.log.log_file("global", basename)
self.log_error("Extended traceback in: %s", tb_path)
with open(tb_path, "wb") as f:
f.write(kobo.tback.Traceback().get_traceback())
f.write(kobo.tback.Traceback(show_locals=show_locals).get_traceback())
def load_old_compose_config(self):
"""

View File

@ -5,11 +5,14 @@ from __future__ import print_function
import os
import six
from collections import namedtuple
from kobo.shortcuts import run
from six.moves import shlex_quote
from .wrappers import iso
from .wrappers.jigdo import JigdoWrapper
from .phases.buildinstall import BOOT_CONFIGS, BOOT_IMAGES
CreateIsoOpts = namedtuple(
"CreateIsoOpts",
@ -118,23 +121,73 @@ def make_jigdo(f, opts):
emit(f, cmd)
def _get_perms(fs_path):
"""Compute proper permissions for a file.
This mimicks what -rational-rock option of genisoimage does. All read bits
are set, so that files and directories are globally readable. If any
execute bit is set for a file, set them all. No writes are allowed and
special bits are erased too.
"""
statinfo = os.stat(fs_path)
perms = 0o444
if statinfo.st_mode & 0o111:
perms |= 0o111
return perms
def write_xorriso_commands(opts):
# Create manifest for the boot.iso listing all contents
boot_iso_manifest = "%s.manifest" % os.path.join(
opts.script_dir, os.path.basename(opts.boot_iso)
)
run(
iso.get_manifest_cmd(
opts.boot_iso, opts.use_xorrisofs, output_file=boot_iso_manifest
)
)
# Find which files may have been updated by pungi. This only includes a few
# files from tweaking buildinstall and .discinfo metadata. There's no good
# way to detect whether the boot config files actually changed, so we may
# be updating files in the ISO with the same data.
UPDATEABLE_FILES = set(BOOT_IMAGES + BOOT_CONFIGS + [".discinfo"])
updated_files = set()
excluded_files = set()
with open(boot_iso_manifest) as f:
for line in f:
path = line.lstrip("/").rstrip("\n")
if path in UPDATEABLE_FILES:
updated_files.add(path)
else:
excluded_files.add(path)
script = os.path.join(opts.script_dir, "xorriso-%s.txt" % id(opts))
with open(script, "w") as f:
emit(f, "-indev %s" % opts.boot_iso)
emit(f, "-outdev %s" % os.path.join(opts.output_dir, opts.iso_name))
emit(f, "-boot_image any replay")
emit(f, "-volid %s" % opts.volid)
# isoinfo -J uses the Joliet tree, and it's used by virt-install
emit(f, "-joliet on")
# Support long filenames in the Joliet trees. Repodata is particularly
# likely to run into this limit.
emit(f, "-compliance joliet_long_names")
with open(opts.graft_points) as gp:
for line in gp:
iso_path, fs_path = line.strip().split("=", 1)
emit(f, "-map %s %s" % (fs_path, iso_path))
if iso_path in excluded_files:
continue
cmd = "-update" if iso_path in updated_files else "-map"
emit(f, "%s %s %s" % (cmd, fs_path, iso_path))
emit(f, "-chmod 0%o %s" % (_get_perms(fs_path), iso_path))
if opts.arch == "ppc64le":
# This is needed for the image to be bootable.
emit(f, "-as mkisofs -U --")
emit(f, "-chown_r 0 /")
emit(f, "-chgrp_r 0 /")
emit(f, "-end")
return script

View File

@ -1118,7 +1118,6 @@ class Pungi(PungiBase):
self.logger.info("Finished gathering package objects.")
def gather(self):
# get package objects according to the input list
self.getPackageObjects()
if self.is_sources:

View File

@ -15,17 +15,20 @@
from enum import Enum
from itertools import count
from functools import cmp_to_key
from itertools import count, groupby
import logging
import os
import re
from kobo.rpmlib import parse_nvra
import rpm
import pungi.common
import pungi.dnf_wrapper
import pungi.multilib_dnf
import pungi.util
from pungi import arch_utils
from pungi.linker import Linker
from pungi.profiler import Profiler
from pungi.util import DEBUG_PATTERNS
@ -245,12 +248,36 @@ class Gather(GatherBase):
# from lookaside. This can be achieved by removing any package that is
# also in lookaside from the list.
lookaside_pkgs = set()
for pkg in package_list:
if pkg.repoid in self.opts.lookaside_repos:
lookaside_pkgs.add("{0.name}-{0.evr}".format(pkg))
if self.opts.greedy_method == "all":
return list(package_list)
if self.opts.lookaside_repos:
# We will call `latest()` to get the highest version packages only.
# However, that is per name and architecture. If a package switches
# from arched to noarch or the other way, it is possible that the
# package_list contains different versions in main repos and in
# lookaside repos.
# We need to manually filter the latest version.
def vercmp(x, y):
return rpm.labelCompare(x[1], y[1])
# Annotate the packages with their version.
versioned_packages = [
(pkg, (str(pkg.epoch) or "0", pkg.version, pkg.release))
for pkg in package_list
]
# Sort the packages newest first.
sorted_packages = sorted(
versioned_packages, key=cmp_to_key(vercmp), reverse=True
)
# Group packages by version, take the first group and discard the
# version info from the tuple.
package_list = list(
x[0] for x in next(groupby(sorted_packages, key=lambda x: x[1]))[1]
)
# Now we can decide what is used from lookaside.
for pkg in package_list:
if pkg.repoid in self.opts.lookaside_repos:
lookaside_pkgs.add("{0.name}-{0.evr}".format(pkg))
all_pkgs = []
for pkg in package_list:
@ -263,16 +290,21 @@ class Gather(GatherBase):
if not debuginfo:
native_pkgs = set(
self.q_native_binary_packages.filter(pkg=all_pkgs).apply()
self.q_native_binary_packages.filter(pkg=all_pkgs).latest().apply()
)
multilib_pkgs = set(
self.q_multilib_binary_packages.filter(pkg=all_pkgs).apply()
self.q_multilib_binary_packages.filter(pkg=all_pkgs).latest().apply()
)
else:
native_pkgs = set(self.q_native_debug_packages.filter(pkg=all_pkgs).apply())
multilib_pkgs = set(
self.q_multilib_debug_packages.filter(pkg=all_pkgs).apply()
native_pkgs = set(
self.q_native_debug_packages.filter(pkg=all_pkgs).latest().apply()
)
multilib_pkgs = set(
self.q_multilib_debug_packages.filter(pkg=all_pkgs).latest().apply()
)
if self.opts.greedy_method == "all":
return list(native_pkgs | multilib_pkgs)
result = set()
@ -392,9 +424,7 @@ class Gather(GatherBase):
"""Given an name of a queue (stored as attribute in `self`), exclude
all given packages and keep only the latest per package name and arch.
"""
setattr(
self, queue, getattr(self, queue).filter(pkg__neq=exclude).latest().apply()
)
setattr(self, queue, getattr(self, queue).filter(pkg__neq=exclude).apply())
@Profiler("Gather._apply_excludes()")
def _apply_excludes(self, excludes):
@ -500,12 +530,21 @@ class Gather(GatherBase):
name__glob=pattern[:-2]
).apply()
else:
pkgs = self.q_binary_packages.filter(
name__glob=pattern
).apply()
kwargs = {"name__glob": pattern}
if "." in pattern:
# The pattern could be name.arch. Check if the
# arch is valid, and if yes, make a more
# specific query.
name, arch = pattern.split(".", 1)
if arch in arch_utils.arches:
kwargs["name__glob"] = name
kwargs["arch__eq"] = arch
pkgs = self.q_binary_packages.filter(**kwargs).apply()
if not pkgs:
self.logger.error("No package matches pattern %s" % pattern)
self.logger.error(
"Could not find a match for %s in any configured repo", pattern
)
# The pattern could have been a glob. In that case we want to
# group the packages by name and get best match in those
@ -616,7 +655,6 @@ class Gather(GatherBase):
return added
for pkg in self.result_debug_packages.copy():
if pkg not in self.finished_add_debug_package_deps:
deps = self._get_package_deps(pkg, debuginfo=True)
for i, req in deps:
@ -784,7 +822,6 @@ class Gather(GatherBase):
continue
debug_pkgs = []
pkg_in_lookaside = pkg.repoid in self.opts.lookaside_repos
for i in candidates:
if pkg.arch != i.arch:
continue
@ -792,7 +829,7 @@ class Gather(GatherBase):
# If it's not debugsource package or does not match name of
# the package, we don't want it in.
continue
if i.repoid in self.opts.lookaside_repos or pkg_in_lookaside:
if self.is_from_lookaside(i):
self._set_flag(i, PkgFlag.lookaside)
if i not in self.result_debug_packages:
added.add(i)

View File

@ -306,11 +306,6 @@ def write_tree_info(compose, arch, variant, timestamp=None, bi=None):
if variant.type in ("addon",) or variant.is_empty:
return
compose.log_debug(
"on arch '%s' looking at variant '%s' of type '%s'"
% (arch, variant, variant.type)
)
if not timestamp:
timestamp = int(time.time())
else:

View File

@ -297,7 +297,7 @@ class BuildinstallPhase(PhaseBase):
"Unsupported buildinstall method: %s" % self.buildinstall_method
)
for (variant, cmd) in commands:
for variant, cmd in commands:
self.pool.add(BuildinstallThread(self.pool))
self.pool.queue_put(
(self.compose, arch, variant, cmd, self.pkgset_phase)
@ -364,9 +364,17 @@ BOOT_CONFIGS = [
"EFI/BOOT/BOOTX64.conf",
"EFI/BOOT/grub.cfg",
]
BOOT_IMAGES = [
"images/efiboot.img",
]
def tweak_configs(path, volid, ks_file, configs=BOOT_CONFIGS, logger=None):
"""
Put escaped volume ID and possibly kickstart file into the boot
configuration files.
:returns: list of paths to modified config files
"""
volid_escaped = volid.replace(" ", r"\x20").replace("\\", "\\\\")
volid_escaped_2 = volid_escaped.replace("\\", "\\\\")
found_configs = []
@ -374,7 +382,6 @@ def tweak_configs(path, volid, ks_file, configs=BOOT_CONFIGS, logger=None):
config_path = os.path.join(path, config)
if not os.path.exists(config_path):
continue
found_configs.append(config)
with open(config_path, "r") as f:
data = original_data = f.read()
@ -394,8 +401,13 @@ def tweak_configs(path, volid, ks_file, configs=BOOT_CONFIGS, logger=None):
with open(config_path, "w") as f:
f.write(data)
if logger and data != original_data:
logger.info("Boot config %s changed" % config_path)
if data != original_data:
found_configs.append(config)
if logger:
# Generally lorax should create file with correct volume id
# already. If we don't have a kickstart, this function should
# be a no-op.
logger.info("Boot config %s changed" % config_path)
return found_configs
@ -434,31 +446,32 @@ def tweak_buildinstall(
if kickstart_file and found_configs:
shutil.copy2(kickstart_file, os.path.join(dst, "ks.cfg"))
images = [
os.path.join(tmp_dir, "images", "efiboot.img"),
]
for image in images:
if not os.path.isfile(image):
continue
images = [os.path.join(tmp_dir, img) for img in BOOT_IMAGES]
if found_configs:
for image in images:
if not os.path.isfile(image):
continue
with iso.mount(
image,
logger=compose._logger,
use_guestmount=compose.conf.get("buildinstall_use_guestmount"),
) as mount_tmp_dir:
for config in BOOT_CONFIGS:
config_path = os.path.join(tmp_dir, config)
config_in_image = os.path.join(mount_tmp_dir, config)
with iso.mount(
image,
logger=compose._logger,
use_guestmount=compose.conf.get("buildinstall_use_guestmount"),
) as mount_tmp_dir:
for config in found_configs:
# Put each modified config file into the image (overwriting the
# original).
config_path = os.path.join(tmp_dir, config)
config_in_image = os.path.join(mount_tmp_dir, config)
if os.path.isfile(config_in_image):
cmd = [
"cp",
"-v",
"--remove-destination",
config_path,
config_in_image,
]
run(cmd)
if os.path.isfile(config_in_image):
cmd = [
"cp",
"-v",
"--remove-destination",
config_path,
config_in_image,
]
run(cmd)
# HACK: make buildinstall files world readable
run("chmod -R a+rX %s" % shlex_quote(tmp_dir))

View File

@ -369,7 +369,7 @@ class CreateisoPhase(PhaseLoggerMixin, PhaseBase):
if self.compose.notifier:
self.compose.notifier.send("createiso-targets", deliverables=deliverables)
for (cmd, variant, arch) in commands:
for cmd, variant, arch in commands:
self.pool.add(CreateIsoThread(self.pool))
self.pool.queue_put((self.compose, cmd, variant, arch))

View File

@ -76,7 +76,7 @@ class ExtraIsosPhase(PhaseLoggerMixin, ConfigGuardedPhase, PhaseBase):
for arch in sorted(arches):
commands.append((config, variant, arch))
for (config, variant, arch) in commands:
for config, variant, arch in commands:
self.pool.add(ExtraIsosThread(self.pool, self.bi))
self.pool.queue_put((self.compose, config, variant, arch))

View File

@ -23,6 +23,7 @@ import threading
from kobo.rpmlib import parse_nvra
from kobo.shortcuts import run
from productmd.rpms import Rpms
from pungi.phases.pkgset.common import get_all_arches
from six.moves import cPickle as pickle
try:
@ -90,7 +91,7 @@ class GatherPhase(PhaseBase):
# check whether variants from configuration value
# 'variant_as_lookaside' are correct
for (requiring, required) in variant_as_lookaside:
for requiring, required in variant_as_lookaside:
if requiring in all_variants and required not in all_variants:
errors.append(
"variant_as_lookaside: variant %r doesn't exist but is "
@ -99,7 +100,7 @@ class GatherPhase(PhaseBase):
# check whether variants from configuration value
# 'variant_as_lookaside' have same architectures
for (requiring, required) in variant_as_lookaside:
for requiring, required in variant_as_lookaside:
if (
requiring in all_variants
and required in all_variants
@ -235,7 +236,7 @@ def reuse_old_gather_packages(compose, arch, variant, package_sets, methods):
if not hasattr(compose, "_gather_reused_variant_arch"):
setattr(compose, "_gather_reused_variant_arch", [])
variant_as_lookaside = compose.conf.get("variant_as_lookaside", [])
for (requiring, required) in variant_as_lookaside:
for requiring, required in variant_as_lookaside:
if (
requiring == variant.uid
and (required, arch) not in compose._gather_reused_variant_arch
@ -468,9 +469,7 @@ def gather_packages(compose, arch, variant, package_sets, fulltree_excludes=None
)
else:
for source_name in ("module", "comps", "json"):
packages, groups, filter_packages = get_variant_packages(
compose, arch, variant, source_name, package_sets
)
@ -575,7 +574,6 @@ def trim_packages(compose, arch, variant, pkg_map, parent_pkgs=None, remove_pkgs
move_to_parent_pkgs = _mk_pkg_map()
removed_pkgs = _mk_pkg_map()
for pkg_type, pkgs in pkg_map.items():
new_pkgs = []
for pkg in pkgs:
pkg_path = pkg["path"]
@ -647,8 +645,14 @@ def _make_lookaside_repo(compose, variant, arch, pkg_map, package_sets=None):
compose.paths.work.topdir(arch="global"), "download"
)
+ "/",
"koji": lambda: pungi.wrappers.kojiwrapper.KojiWrapper(
compose
"koji": lambda: compose.conf.get(
"koji_cache",
pungi.wrappers.kojiwrapper.KojiWrapper(compose).koji_module.config.topdir,
).rstrip("/")
+ "/",
"kojimock": lambda: pungi.wrappers.kojiwrapper.KojiMockWrapper(
compose,
get_all_arches(compose),
).koji_module.config.topdir.rstrip("/")
+ "/",
}

View File

@ -47,9 +47,15 @@ class FakePackage(object):
@property
def files(self):
return [
os.path.join(dirname, basename) for (_, dirname, basename) in self.pkg.files
]
paths = []
# createrepo_c.Package.files is a tuple, but its length differs across
# versions. The constants define index at which the related value is
# located.
for entry in self.pkg.files:
paths.append(
os.path.join(entry[cr.FILE_ENTRY_PATH], entry[cr.FILE_ENTRY_NAME])
)
return paths
@property
def provides(self):

View File

@ -25,6 +25,7 @@ from productmd.rpms import Rpms
# results will be pulled into the compose.
EXTENSIONS = {
"docker": ["tar.gz", "tar.xz"],
"iso": ["iso"],
"liveimg-squashfs": ["liveimg.squashfs"],
"qcow": ["qcow"],
"qcow2": ["qcow2"],
@ -39,6 +40,7 @@ EXTENSIONS = {
"vdi": ["vdi"],
"vmdk": ["vmdk"],
"vpc": ["vhd"],
"vhd-compressed": ["vhd.gz", "vhd.xz"],
"vsphere-ova": ["vsphere.ova"],
}
@ -344,7 +346,9 @@ class CreateImageBuildThread(WorkerThread):
# let's not change filename of koji outputs
image_dest = os.path.join(image_dir, os.path.basename(image_info["path"]))
src_file = os.path.realpath(image_info["path"])
src_file = compose.koji_downloader.get_file(
os.path.realpath(image_info["path"])
)
linker.link(src_file, image_dest, link_type=cmd["link_type"])
# Update image manifest

View File

@ -117,7 +117,7 @@ class LiveImagesPhase(
commands.append((cmd, variant, arch))
for (cmd, variant, arch) in commands:
for cmd, variant, arch in commands:
self.pool.add(CreateLiveImageThread(self.pool))
self.pool.queue_put((self.compose, cmd, variant, arch))
@ -232,7 +232,7 @@ class CreateLiveImageThread(WorkerThread):
"Got %d images from task %d, expected 1."
% (len(image_path), output["task_id"])
)
image_path = image_path[0]
image_path = compose.koji_downloader.get_file(image_path[0])
filename = cmd.get("filename") or os.path.basename(image_path)
destination = os.path.join(cmd["dest_dir"], filename)
shutil.copy2(image_path, destination)

View File

@ -182,7 +182,9 @@ class LiveMediaThread(WorkerThread):
# let's not change filename of koji outputs
image_dest = os.path.join(image_dir, os.path.basename(image_info["path"]))
src_file = os.path.realpath(image_info["path"])
src_file = compose.koji_downloader.get_file(
os.path.realpath(image_info["path"])
)
linker.link(src_file, image_dest, link_type=link_type)
# Update image manifest

View File

@ -27,6 +27,35 @@ class OSBuildPhase(
arches = set(image_conf["arches"]) & arches
return sorted(arches)
@staticmethod
def _get_repo_urls(compose, repos, arch="$basearch"):
"""
Get list of repos with resolved repo URLs. Preserve repos defined
as dicts.
"""
resolved_repos = []
for repo in repos:
if isinstance(repo, dict):
try:
url = repo["baseurl"]
except KeyError:
raise RuntimeError(
"`baseurl` is required in repo dict %s" % str(repo)
)
url = util.get_repo_url(compose, url, arch=arch)
if url is None:
raise RuntimeError("Failed to resolve repo URL for %s" % str(repo))
repo["baseurl"] = url
resolved_repos.append(repo)
else:
repo = util.get_repo_url(compose, repo, arch=arch)
if repo is None:
raise RuntimeError("Failed to resolve repo URL for %s" % repo)
resolved_repos.append(repo)
return resolved_repos
def _get_repo(self, image_conf, variant):
"""
Get a list of repos. First included are those explicitly listed in
@ -38,7 +67,7 @@ class OSBuildPhase(
if not variant.is_empty and variant.uid not in repos:
repos.append(variant.uid)
return util.get_repo_urls(self.compose, repos, arch="$arch")
return OSBuildPhase._get_repo_urls(self.compose, repos, arch="$arch")
def run(self):
for variant in self.compose.get_variants():
@ -183,16 +212,27 @@ class RunOSBuildThread(WorkerThread):
# image_dir is absolute path to which the image should be copied.
# We also need the same path as relative to compose directory for
# including in the metadata.
image_dir = compose.paths.compose.image_dir(variant) % {"arch": arch}
rel_image_dir = compose.paths.compose.image_dir(variant, relative=True) % {
"arch": arch
}
if archive["type_name"] == "iso":
# If the produced image is actually an ISO, it should go to
# iso/ subdirectory.
image_dir = compose.paths.compose.iso_dir(arch, variant)
rel_image_dir = compose.paths.compose.iso_dir(
arch, variant, relative=True
)
else:
image_dir = compose.paths.compose.image_dir(variant) % {"arch": arch}
rel_image_dir = compose.paths.compose.image_dir(
variant, relative=True
) % {"arch": arch}
util.makedirs(image_dir)
image_dest = os.path.join(image_dir, archive["filename"])
src_file = os.path.join(
koji.koji_module.pathinfo.imagebuild(build_info), archive["filename"]
src_file = compose.koji_downloader.get_file(
os.path.join(
koji.koji_module.pathinfo.imagebuild(build_info),
archive["filename"],
),
)
linker.link(src_file, image_dest, link_type=compose.conf["link_type"])
@ -209,7 +249,7 @@ class RunOSBuildThread(WorkerThread):
# Update image manifest
img = Image(compose.im)
img.type = archive["type_name"]
img.type = archive["type_name"] if archive["type_name"] != "iso" else "dvd"
img.format = suffix
img.path = os.path.join(rel_image_dir, archive["filename"])
img.mtime = util.get_mtime(image_dest)

View File

@ -168,7 +168,9 @@ class OSTreeThread(WorkerThread):
("unified-core", config.get("unified_core", False)),
]
)
packages = ["pungi", "ostree", "rpm-ostree"]
default_packages = ["pungi", "ostree", "rpm-ostree"]
additional_packages = config.get("runroot_packages", [])
packages = default_packages + additional_packages
log_file = os.path.join(self.logdir, "runroot.log")
mounts = [compose.topdir, config["ostree_repo"]]
runroot = Runroot(compose, phase="ostree")

View File

@ -38,12 +38,17 @@ from pungi.phases.createrepo import add_modular_metadata
def populate_arch_pkgsets(compose, path_prefix, global_pkgset):
result = {}
exclusive_noarch = compose.conf["pkgset_exclusive_arch_considers_noarch"]
for arch in compose.get_arches():
compose.log_info("Populating package set for arch: %s", arch)
is_multilib = is_arch_multilib(compose.conf, arch)
arches = get_valid_arches(arch, is_multilib, add_src=True)
pkgset = global_pkgset.subset(arch, arches, exclusive_noarch=exclusive_noarch)
pkgset = global_pkgset.subset(
arch,
arches,
exclusive_noarch=compose.conf["pkgset_exclusive_arch_considers_noarch"],
inherit_to_noarch=compose.conf["pkgset_inherit_exclusive_arch_to_noarch"],
)
pkgset.save_file_list(
compose.paths.work.package_list(arch=arch, pkgset=global_pkgset),
remove_path_prefix=path_prefix,

View File

@ -23,11 +23,15 @@ import itertools
import json
import os
import time
import pgpy
import rpm
from six.moves import cPickle as pickle
from functools import partial
import kobo.log
import kobo.pkgset
import kobo.rpmlib
from kobo.shortcuts import compute_file_checksums
from kobo.threads import WorkerThread, ThreadPool
@ -150,9 +154,15 @@ class PackageSetBase(kobo.log.LoggingBase):
"""
def nvr_formatter(package_info):
# joins NVR parts of the package with '-' character.
return "-".join(
(package_info["name"], package_info["version"], package_info["release"])
epoch_suffix = ''
if package_info['epoch'] is not None:
epoch_suffix = ':' + package_info['epoch']
return (
f"{package_info['name']}"
f"{epoch_suffix}-"
f"{package_info['version']}-"
f"{package_info['release']}."
f"{package_info['arch']}"
)
def get_error(sigkeys, infos):
@ -203,16 +213,31 @@ class PackageSetBase(kobo.log.LoggingBase):
return self.rpms_by_arch
def subset(self, primary_arch, arch_list, exclusive_noarch=True):
def subset(
self, primary_arch, arch_list, exclusive_noarch=True, inherit_to_noarch=True
):
"""Create a subset of this package set that only includes
packages compatible with"""
pkgset = PackageSetBase(
self.name, self.sigkey_ordering, logger=self._logger, arches=arch_list
)
pkgset.merge(self, primary_arch, arch_list, exclusive_noarch=exclusive_noarch)
pkgset.merge(
self,
primary_arch,
arch_list,
exclusive_noarch=exclusive_noarch,
inherit_to_noarch=inherit_to_noarch,
)
return pkgset
def merge(self, other, primary_arch, arch_list, exclusive_noarch=True):
def merge(
self,
other,
primary_arch,
arch_list,
exclusive_noarch=True,
inherit_to_noarch=True,
):
"""
Merge ``other`` package set into this instance.
"""
@ -251,7 +276,7 @@ class PackageSetBase(kobo.log.LoggingBase):
if i.file_path in self.file_cache:
# TODO: test if it really works
continue
if exclusivearch_list and arch == "noarch":
if inherit_to_noarch and exclusivearch_list and arch == "noarch":
if is_excluded(i, exclusivearch_list, logger=self._logger):
continue
@ -318,6 +343,11 @@ class FilelistPackageSet(PackageSetBase):
return result
# This is a marker to indicate package set with only extra builds/tasks and no
# tasks.
MISSING_KOJI_TAG = object()
class KojiPackageSet(PackageSetBase):
def __init__(
self,
@ -334,6 +364,7 @@ class KojiPackageSet(PackageSetBase):
extra_tasks=None,
signed_packages_retries=0,
signed_packages_wait=30,
downloader=None,
):
"""
Creates new KojiPackageSet.
@ -371,7 +402,7 @@ class KojiPackageSet(PackageSetBase):
:param int signed_packages_wait: How long to wait between search attemts.
"""
super(KojiPackageSet, self).__init__(
name,
name if name != MISSING_KOJI_TAG else "no-tag",
sigkey_ordering=sigkey_ordering,
arches=arches,
logger=logger,
@ -388,6 +419,8 @@ class KojiPackageSet(PackageSetBase):
self.signed_packages_retries = signed_packages_retries
self.signed_packages_wait = signed_packages_wait
self.downloader = downloader
def __getstate__(self):
result = self.__dict__.copy()
del result["koji_wrapper"]
@ -478,7 +511,8 @@ class KojiPackageSet(PackageSetBase):
response = None
if self.cache_region:
cache_key = "KojiPackageSet.get_latest_rpms_%s_%s_%s" % (
cache_key = "%s.get_latest_rpms_%s_%s_%s" % (
str(self.__class__.__name__),
str(tag),
str(event),
str(inherit),
@ -508,11 +542,28 @@ class KojiPackageSet(PackageSetBase):
# Check if this RPM is coming from scratch task. In this case, we already
# know the path.
if "path_from_task" in rpm_info:
return rpm_info["path_from_task"]
return self.downloader.get_file(rpm_info["path_from_task"])
pathinfo = self.koji_wrapper.koji_module.pathinfo
paths = []
if "getRPMChecksums" in self.koji_proxy.system.listMethods():
def checksum_validator(keyname, pkg_path):
checksums = self.koji_proxy.getRPMChecksums(
rpm_info["id"], checksum_types=("sha256",)
)
if "sha256" in checksums.get(keyname, {}):
computed = compute_file_checksums(pkg_path, ("sha256",))
if computed["sha256"] != checksums[keyname]["sha256"]:
raise RuntimeError("Checksum mismatch for %s" % pkg_path)
else:
def checksum_validator(keyname, pkg_path):
# Koji doesn't support checksums yet
pass
attempts_left = self.signed_packages_retries + 1
while attempts_left > 0:
for sigkey in self.sigkey_ordering:
@ -525,8 +576,11 @@ class KojiPackageSet(PackageSetBase):
)
if rpm_path not in paths:
paths.append(rpm_path)
if os.path.isfile(rpm_path):
return rpm_path
path = self.downloader.get_file(
rpm_path, partial(checksum_validator, sigkey)
)
if path:
return path
# No signed copy was found, wait a little and try again.
attempts_left -= 1
@ -539,16 +593,18 @@ class KojiPackageSet(PackageSetBase):
# use an unsigned copy (if allowed)
rpm_path = os.path.join(pathinfo.build(build_info), pathinfo.rpm(rpm_info))
paths.append(rpm_path)
if os.path.isfile(rpm_path):
return rpm_path
path = self.downloader.get_file(rpm_path, partial(checksum_validator, ""))
if path:
return path
if self._allow_invalid_sigkeys and rpm_info["name"] not in self.packages:
# use an unsigned copy (if allowed)
rpm_path = os.path.join(pathinfo.build(build_info), pathinfo.rpm(rpm_info))
paths.append(rpm_path)
if os.path.isfile(rpm_path):
path = self.downloader.get_file(rpm_path)
if path:
self._invalid_sigkey_rpms.append(rpm_info)
return rpm_path
return path
self._invalid_sigkey_rpms.append(rpm_info)
self.log_error(
@ -578,7 +634,9 @@ class KojiPackageSet(PackageSetBase):
inherit,
)
self.log_info("[BEGIN] %s" % msg)
rpms, builds = self.get_latest_rpms(tag, event, inherit=inherit)
rpms, builds = [], []
if tag != MISSING_KOJI_TAG:
rpms, builds = self.get_latest_rpms(tag, event, inherit=inherit)
extra_rpms, extra_builds = self.get_extra_rpms()
rpms += extra_rpms
builds += extra_builds
@ -683,6 +741,15 @@ class KojiPackageSet(PackageSetBase):
:param include_packages: an iterable of tuples (package name, arch) that should
be included.
"""
if len(self.sigkey_ordering) > 1 and (
None in self.sigkey_ordering or "" in self.sigkey_ordering
):
self.log_warning(
"Stop writing reuse file as unsigned packages are allowed "
"in the compose."
)
return
reuse_file = compose.paths.work.pkgset_reuse_file(self.name)
self.log_info("Writing pkgset reuse file: %s" % reuse_file)
try:
@ -699,6 +766,12 @@ class KojiPackageSet(PackageSetBase):
"srpms_by_name": self.srpms_by_name,
"extra_builds": self.extra_builds,
"include_packages": include_packages,
"inherit_to_noarch": compose.conf[
"pkgset_inherit_exclusive_arch_to_noarch"
],
"exclusive_noarch": compose.conf[
"pkgset_exclusive_arch_considers_noarch"
],
},
f,
protocol=pickle.HIGHEST_PROTOCOL,
@ -793,6 +866,8 @@ class KojiPackageSet(PackageSetBase):
self.log_debug("Failed to load reuse file: %s" % str(e))
return False
inherit_to_noarch = compose.conf["pkgset_inherit_exclusive_arch_to_noarch"]
exclusive_noarch = compose.conf["pkgset_exclusive_arch_considers_noarch"]
if (
reuse_data["allow_invalid_sigkeys"] == self._allow_invalid_sigkeys
and reuse_data["packages"] == self.packages
@ -800,6 +875,10 @@ class KojiPackageSet(PackageSetBase):
and reuse_data["extra_builds"] == self.extra_builds
and reuse_data["sigkeys"] == self.sigkey_ordering
and reuse_data["include_packages"] == include_packages
# If the value is not present in reuse data, the compose was
# generated with older version of Pungi. Best to not reuse.
and reuse_data.get("inherit_to_noarch") == inherit_to_noarch
and reuse_data.get("exclusive_noarch") == exclusive_noarch
):
self.log_info("Copying repo data for reuse: %s" % old_repo_dir)
copy_all(old_repo_dir, repo_dir)
@ -814,190 +893,33 @@ class KojiPackageSet(PackageSetBase):
return False
class KojiMockPackageSet(PackageSetBase):
def __init__(
self,
name,
koji_wrapper,
sigkey_ordering,
arches=None,
logger=None,
packages=None,
allow_invalid_sigkeys=False,
populate_only_packages=False,
cache_region=None,
extra_builds=None,
extra_tasks=None,
signed_packages_retries=0,
signed_packages_wait=30,
):
"""
Creates new KojiPackageSet.
class KojiMockPackageSet(KojiPackageSet):
:param list sigkey_ordering: Ordered list of sigkey strings. When
getting package from Koji, KojiPackageSet tries to get the package
signed by sigkey from this list. If None or "" appears in this
list, unsigned package is used.
:param list arches: List of arches to get the packages for.
:param logging.Logger logger: Logger instance to use for logging.
:param list packages: List of package names to be used when
`allow_invalid_sigkeys` or `populate_only_packages` is set.
:param bool allow_invalid_sigkeys: When True, packages *not* listed in
the `packages` list are added to KojiPackageSet even if they have
invalid sigkey. This is useful in case Koji tag contains some
unsigned packages, but we know they won't appear in a compose.
When False, all packages in Koji tag must have valid sigkey as
defined in `sigkey_ordering`.
:param bool populate_only_packages. When True, only packages in
`packages` list are added to KojiPackageSet. This can save time
when generating compose from predefined list of packages from big
Koji tag.
When False, all packages from Koji tag are added to KojiPackageSet.
:param dogpile.cache.CacheRegion cache_region: If set, the CacheRegion
will be used to cache the list of RPMs per Koji tag, so next calls
of the KojiPackageSet.populate(...) method won't try fetching it
again.
:param list extra_builds: Extra builds NVRs to get from Koji and include
in the package set.
:param list extra_tasks: Extra RPMs defined as Koji task IDs to get from Koji
and include in the package set. Useful when building testing compose
with RPM scratch builds.
"""
super(KojiMockPackageSet , self).__init__(
name,
sigkey_ordering=sigkey_ordering,
arches=arches,
logger=logger,
allow_invalid_sigkeys=allow_invalid_sigkeys,
)
self.koji_wrapper = koji_wrapper
# Names of packages to look for in the Koji tag.
self.packages = set(packages or [])
self.populate_only_packages = populate_only_packages
self.cache_region = cache_region
self.extra_builds = extra_builds or []
self.extra_tasks = extra_tasks or []
self.reuse = None
self.signed_packages_retries = signed_packages_retries
self.signed_packages_wait = signed_packages_wait
def __getstate__(self):
result = self.__dict__.copy()
del result["koji_wrapper"]
del result["_logger"]
if "cache_region" in result:
del result["cache_region"]
return result
def __setstate__(self, data):
self._logger = None
self.__dict__.update(data)
@property
def koji_proxy(self):
return self.koji_wrapper.koji_proxy
def get_extra_rpms(self):
if not self.extra_builds:
return [], []
rpms = []
builds = []
builds = self.koji_wrapper.retrying_multicall_map(
self.koji_proxy, self.koji_proxy.getBuild, list_of_args=self.extra_builds
)
rpms_in_builds = self.koji_wrapper.retrying_multicall_map(
self.koji_proxy,
self.koji_proxy.listBuildRPMs,
list_of_args=self.extra_builds,
)
rpms = []
for rpms_in_build in rpms_in_builds:
rpms += rpms_in_build
return rpms, builds
def get_extra_rpms_from_tasks(self):
"""
Returns manually constructed RPM infos from the Koji tasks defined
in `self.extra_tasks`.
:rtype: list
:return: List with RPM infos defined as dicts with following keys:
- name, version, release, arch, src - as returned by parse_nvra.
- path_from_task - Full path to RPM on /mnt/koji.
- build_id - Always set to None.
"""
if not self.extra_tasks:
return []
# Get the IDs of children tasks - these are the tasks containing
# the resulting RPMs.
children_tasks = self.koji_wrapper.retrying_multicall_map(
self.koji_proxy,
self.koji_proxy.getTaskChildren,
list_of_args=self.extra_tasks,
)
children_task_ids = []
for tasks in children_tasks:
children_task_ids += [t["id"] for t in tasks]
# Get the results of these children tasks.
results = self.koji_wrapper.retrying_multicall_map(
self.koji_proxy,
self.koji_proxy.getTaskResult,
list_of_args=children_task_ids,
)
rpms = []
for result in results:
rpms += result.get("rpms", [])
rpms += result.get("srpms", [])
rpm_infos = []
for rpm in rpms:
rpm_info = kobo.rpmlib.parse_nvra(os.path.basename(rpm))
rpm_info["path_from_task"] = os.path.join(
self.koji_wrapper.koji_module.pathinfo.work(), rpm
def _is_rpm_signed(self, rpm_path) -> bool:
ts = rpm.TransactionSet()
ts.setVSFlags(rpm._RPMVSF_NOSIGNATURES)
sigkeys = [
sigkey.lower() for sigkey in self.sigkey_ordering
if sigkey is not None
]
if not sigkeys:
return True
with open(rpm_path, 'rb') as fd:
header = ts.hdrFromFdno(fd)
signature = header[rpm.RPMTAG_SIGGPG] or header[rpm.RPMTAG_SIGPGP]
if signature is None:
return False
pgp_msg = pgpy.PGPMessage.from_blob(signature)
return any(
signature.signer.lower() in sigkeys
for signature in pgp_msg.signatures
)
rpm_info["build_id"] = None
rpm_infos.append(rpm_info)
return rpm_infos
def get_latest_rpms(self, tag, event, inherit=True):
if not tag:
return [], []
response = None
if self.cache_region:
cache_key = "KojiPackageSet.get_latest_rpms_%s_%s_%s" % (
str(tag),
str(event),
str(inherit),
)
try:
response = self.cache_region.get(cache_key)
except Exception:
pass
if not response:
response = self.koji_proxy.listTaggedRPMS(
tag, event=event, inherit=inherit, latest=True
)
if self.cache_region:
try:
self.cache_region.set(cache_key, response)
except Exception:
pass
return response
def get_package_path(self, queue_item):
rpm_info, build_info = queue_item
# Check if this RPM is coming from scratch task. In this case, we already
# know the path.
# Check if this RPM is coming from scratch task.
# In this case, we already know the path.
if "path_from_task" in rpm_info:
return rpm_info["path_from_task"]
@ -1010,267 +932,27 @@ class KojiMockPackageSet(PackageSetBase):
rpm_path = os.path.join(pathinfo.topdir, pathinfo.rpm(rpm_info))
if os.path.isfile(rpm_path):
if not self._is_rpm_signed(rpm_path):
self._invalid_sigkey_rpms.append(rpm_info)
self.log_error(
'RPM "%s" not found for sigs: "%s". Path checked: "%s"',
rpm_info, self.sigkey_ordering, rpm_path
)
return
return rpm_path
else:
self.log_warning("RPM %s not found" % rpm_path)
return None
def populate(self, tag, event=None, inherit=True, include_packages=None):
"""Populate the package set with packages from given tag.
:param event: the Koji event to query at (or latest if not given)
:param inherit: whether to enable tag inheritance
:param include_packages: an iterable of tuples (package name, arch) that should
be included, all others are skipped.
"""
result_rpms = []
result_srpms = []
include_packages = set(include_packages or [])
if type(event) is dict:
event = event["id"]
msg = "Getting latest RPMs (tag: %s, event: %s, inherit: %s)" % (
tag,
event,
inherit,
result = super().populate(
tag=tag,
event=event,
inherit=inherit,
include_packages=include_packages,
)
self.log_info("[BEGIN] %s" % msg)
rpms, builds = self.get_latest_rpms(tag, event, inherit=inherit)
extra_rpms, extra_builds = self.get_extra_rpms()
rpms += extra_rpms
builds += extra_builds
extra_builds_by_name = {}
for build_info in extra_builds:
extra_builds_by_name[build_info["name"]] = build_info["build_id"]
builds_by_id = {}
exclude_build_id = []
for build_info in builds:
build_id, build_name = build_info["build_id"], build_info["name"]
if (
build_name in extra_builds_by_name
and build_id != extra_builds_by_name[build_name]
):
exclude_build_id.append(build_id)
else:
builds_by_id.setdefault(build_id, build_info)
# Get extra RPMs from tasks.
rpms += self.get_extra_rpms_from_tasks()
skipped_arches = []
skipped_packages_count = 0
# We need to process binary packages first, and then source packages.
# If we have a list of packages to use, we need to put all source rpms
# names into it. Otherwise if the SRPM name does not occur on the list,
# it would be missing from the package set. Even if it ultimately does
# not end in the compose, we need it to extract ExcludeArch and
# ExclusiveArch for noarch packages.
for rpm_info in itertools.chain(
(rpm for rpm in rpms if not _is_src(rpm)),
(rpm for rpm in rpms if _is_src(rpm)),
):
if rpm_info["build_id"] in exclude_build_id:
continue
if self.arches and rpm_info["arch"] not in self.arches:
if rpm_info["arch"] not in skipped_arches:
self.log_debug("Skipping packages for arch: %s" % rpm_info["arch"])
skipped_arches.append(rpm_info["arch"])
continue
if (
include_packages
and (rpm_info["name"], rpm_info["arch"]) not in include_packages
and rpm_info["arch"] != "src"
):
self.log_debug(
"Skipping %(name)s-%(version)s-%(release)s.%(arch)s" % rpm_info
)
continue
if (
self.populate_only_packages
and self.packages
and rpm_info["name"] not in self.packages
):
skipped_packages_count += 1
continue
build_info = builds_by_id.get(rpm_info["build_id"], None)
if _is_src(rpm_info):
result_srpms.append((rpm_info, build_info))
else:
result_rpms.append((rpm_info, build_info))
if self.populate_only_packages and self.packages:
# Only add the package if we already have some whitelist.
if build_info:
self.packages.add(build_info["name"])
else:
# We have no build info and therefore no Koji package name,
# we can only guess that the Koji package name would be the same
# one as the RPM name.
self.packages.add(rpm_info["name"])
if skipped_packages_count:
self.log_debug(
"Skipped %d packages, not marked as to be "
"included in a compose." % skipped_packages_count
)
result = self.read_packages(result_rpms, result_srpms)
# Check that after reading the packages, every package that is
# included in a compose has the right sigkey.
if self._invalid_sigkey_rpms:
invalid_sigkey_rpms = [
rpm for rpm in self._invalid_sigkey_rpms if rpm["name"] in self.packages
]
if invalid_sigkey_rpms:
self.raise_invalid_sigkeys_exception(invalid_sigkey_rpms)
self.log_info("[DONE ] %s" % msg)
return result
def write_reuse_file(self, compose, include_packages):
"""Write data to files for reusing in future.
:param compose: compose object
:param include_packages: an iterable of tuples (package name, arch) that should
be included.
"""
reuse_file = compose.paths.work.pkgset_reuse_file(self.name)
self.log_info("Writing pkgset reuse file: %s" % reuse_file)
try:
with open(reuse_file, "wb") as f:
pickle.dump(
{
"name": self.name,
"allow_invalid_sigkeys": self._allow_invalid_sigkeys,
"arches": self.arches,
"sigkeys": self.sigkey_ordering,
"packages": self.packages,
"populate_only_packages": self.populate_only_packages,
"rpms_by_arch": self.rpms_by_arch,
"srpms_by_name": self.srpms_by_name,
"extra_builds": self.extra_builds,
"include_packages": include_packages,
},
f,
protocol=pickle.HIGHEST_PROTOCOL,
)
except Exception as e:
self.log_warning("Writing pkgset reuse file failed: %s" % str(e))
def _get_koji_event_from_file(self, event_file):
with open(event_file, "r") as f:
return json.load(f)["id"]
def try_to_reuse(self, compose, tag, inherit=True, include_packages=None):
"""Try to reuse pkgset data of old compose.
:param compose: compose object
:param str tag: koji tag name
:param inherit: whether to enable tag inheritance
:param include_packages: an iterable of tuples (package name, arch) that should
be included.
"""
if not compose.conf["pkgset_allow_reuse"]:
self.log_info("Reusing pkgset data from old compose is disabled.")
return False
self.log_info("Trying to reuse pkgset data of old compose")
if not compose.paths.get_old_compose_topdir():
self.log_debug("No old compose found. Nothing to reuse.")
return False
event_file = os.path.join(
compose.paths.work.topdir(arch="global", create_dir=False), "koji-event"
)
old_event_file = compose.paths.old_compose_path(event_file)
try:
koji_event = self._get_koji_event_from_file(event_file)
old_koji_event = self._get_koji_event_from_file(old_event_file)
except Exception as e:
self.log_debug("Can't read koji event from file: %s" % str(e))
return False
if koji_event != old_koji_event:
self.log_debug(
"Koji event doesn't match, querying changes between event %d and %d"
% (old_koji_event, koji_event)
)
changed = self.koji_proxy.queryHistory(
tables=["tag_listing", "tag_inheritance"],
tag=tag,
afterEvent=min(koji_event, old_koji_event),
beforeEvent=max(koji_event, old_koji_event) + 1,
)
if changed["tag_listing"]:
self.log_debug("Builds under tag %s changed. Can't reuse." % tag)
return False
if changed["tag_inheritance"]:
self.log_debug("Tag inheritance %s changed. Can't reuse." % tag)
return False
if inherit:
inherit_tags = self.koji_proxy.getFullInheritance(tag, koji_event)
for t in inherit_tags:
changed = self.koji_proxy.queryHistory(
tables=["tag_listing", "tag_inheritance"],
tag=t["name"],
afterEvent=min(koji_event, old_koji_event),
beforeEvent=max(koji_event, old_koji_event) + 1,
)
if changed["tag_listing"]:
self.log_debug(
"Builds under inherited tag %s changed. Can't reuse."
% t["name"]
)
return False
if changed["tag_inheritance"]:
self.log_debug("Tag inheritance %s changed. Can't reuse." % tag)
return False
repo_dir = compose.paths.work.pkgset_repo(tag, create_dir=False)
old_repo_dir = compose.paths.old_compose_path(repo_dir)
if not old_repo_dir:
self.log_debug("Can't find old repo dir to reuse.")
return False
old_reuse_file = compose.paths.old_compose_path(
compose.paths.work.pkgset_reuse_file(tag)
)
try:
self.log_debug("Loading reuse file: %s" % old_reuse_file)
reuse_data = self.load_old_file_cache(old_reuse_file)
except Exception as e:
self.log_debug("Failed to load reuse file: %s" % str(e))
return False
if (
reuse_data["allow_invalid_sigkeys"] == self._allow_invalid_sigkeys
and reuse_data["packages"] == self.packages
and reuse_data["populate_only_packages"] == self.populate_only_packages
and reuse_data["extra_builds"] == self.extra_builds
and reuse_data["sigkeys"] == self.sigkey_ordering
and reuse_data["include_packages"] == include_packages
):
self.log_info("Copying repo data for reuse: %s" % old_repo_dir)
copy_all(old_repo_dir, repo_dir)
self.reuse = old_repo_dir
self.rpms_by_arch = reuse_data["rpms_by_arch"]
self.srpms_by_name = reuse_data["srpms_by_name"]
if self.old_file_cache:
self.file_cache = self.old_file_cache
return True
else:
self.log_info("Criteria does not match. Nothing to reuse.")
return False
def _is_src(rpm_info):
"""Check if rpm info object returned by Koji refers to source packages."""

View File

@ -15,8 +15,10 @@
from .source_koji import PkgsetSourceKoji
from .source_repos import PkgsetSourceRepos
from .source_kojimock import PkgsetSourceKojiMock
ALL_SOURCES = {
"koji": PkgsetSourceKoji,
"repos": PkgsetSourceRepos,
"kojimock": PkgsetSourceKojiMock,
}

View File

@ -193,17 +193,13 @@ class PkgsetSourceKoji(pungi.phases.pkgset.source.PkgsetSourceBase):
def __call__(self):
compose = self.compose
self.koji_wrapper = pungi.wrappers.kojiwrapper.KojiWrapper(compose)
# path prefix must contain trailing '/'
path_prefix = self.koji_wrapper.koji_module.config.topdir.rstrip("/") + "/"
package_sets = get_pkgset_from_koji(
self.compose, self.koji_wrapper, path_prefix
)
return (package_sets, path_prefix)
package_sets = get_pkgset_from_koji(self.compose, self.koji_wrapper)
return (package_sets, self.compose.koji_downloader.path_prefix)
def get_pkgset_from_koji(compose, koji_wrapper, path_prefix):
def get_pkgset_from_koji(compose, koji_wrapper):
event_info = get_koji_event_info(compose, koji_wrapper)
return populate_global_pkgset(compose, koji_wrapper, path_prefix, event_info)
return populate_global_pkgset(compose, koji_wrapper, event_info)
def _add_module_to_variant(
@ -232,7 +228,7 @@ def _add_module_to_variant(
continue
typedir = koji_wrapper.koji_module.pathinfo.typedir(build, archive["btype"])
filename = archive["filename"]
file_path = os.path.join(typedir, filename)
file_path = compose.koji_downloader.get_file(os.path.join(typedir, filename))
try:
# If there are two dots, the arch is in the middle. MBS uploads
# files with actual architecture in the filename, but Pungi deals
@ -270,9 +266,14 @@ def _add_module_to_variant(
"Module %s does not have metadata for arch %s and is not filtered "
"out via filter_modules option." % (nsvc, arch)
)
mod_stream = read_single_module_stream_from_file(
mmds[filename], compose, arch, build
)
try:
mod_stream = read_single_module_stream_from_file(
mmds[filename], compose, arch, build
)
except Exception as exc:
# libmodulemd raises various GLib exceptions with not very helpful
# messages. Let's replace it with something more useful.
raise RuntimeError("Failed to read %s: %s", mmds[filename], str(exc))
if mod_stream:
added = True
variant.arch_mmds.setdefault(arch, {})[nsvc] = mod_stream
@ -395,7 +396,13 @@ def _is_filtered_out(compose, variant, arch, module_name, module_stream):
def _get_modules_from_koji(
compose, koji_wrapper, event, variant, variant_tags, tag_to_mmd, exclude_module_ns
compose,
koji_wrapper,
event,
variant,
variant_tags,
tag_to_mmd,
exclude_module_ns,
):
"""
Loads modules for given `variant` from koji `session`, adds them to
@ -670,7 +677,7 @@ def _get_modules_from_koji_tags(
)
def populate_global_pkgset(compose, koji_wrapper, path_prefix, event):
def populate_global_pkgset(compose, koji_wrapper, event):
all_arches = get_all_arches(compose)
# List of compose tags from which we create this compose
@ -764,7 +771,12 @@ def populate_global_pkgset(compose, koji_wrapper, path_prefix, event):
if extra_modules:
_add_extra_modules_to_variant(
compose, koji_wrapper, variant, extra_modules, variant_tags, tag_to_mmd
compose,
koji_wrapper,
variant,
extra_modules,
variant_tags,
tag_to_mmd,
)
variant_scratch_modules = get_variant_data(
@ -791,17 +803,23 @@ def populate_global_pkgset(compose, koji_wrapper, path_prefix, event):
pkgsets = []
extra_builds = force_list(compose.conf.get("pkgset_koji_builds", []))
extra_tasks = force_list(compose.conf.get("pkgset_koji_scratch_tasks", []))
if not pkgset_koji_tags and (extra_builds or extra_tasks):
# We have extra packages to pull in, but no tag to merge them with.
compose_tags.append(pungi.phases.pkgset.pkgsets.MISSING_KOJI_TAG)
pkgset_koji_tags.append(pungi.phases.pkgset.pkgsets.MISSING_KOJI_TAG)
# Get package set for each compose tag and merge it to global package
# list. Also prepare per-variant pkgset, because we do not have list
# of binary RPMs in module definition - there is just list of SRPMs.
for compose_tag in compose_tags:
compose.log_info("Loading package set for tag %s", compose_tag)
kwargs = {}
if compose_tag in pkgset_koji_tags:
extra_builds = force_list(compose.conf.get("pkgset_koji_builds", []))
extra_tasks = force_list(compose.conf.get("pkgset_koji_scratch_tasks", []))
else:
extra_builds = []
extra_tasks = []
kwargs["extra_builds"] = extra_builds
kwargs["extra_tasks"] = extra_tasks
pkgset = pungi.phases.pkgset.pkgsets.KojiPackageSet(
compose_tag,
@ -813,10 +831,10 @@ def populate_global_pkgset(compose, koji_wrapper, path_prefix, event):
allow_invalid_sigkeys=allow_invalid_sigkeys,
populate_only_packages=populate_only_packages_to_gather,
cache_region=compose.cache_region,
extra_builds=extra_builds,
extra_tasks=extra_tasks,
signed_packages_retries=compose.conf["signed_packages_retries"],
signed_packages_wait=compose.conf["signed_packages_wait"],
downloader=compose.koji_downloader,
**kwargs
)
# Check if we have cache for this tag from previous compose. If so, use
@ -880,7 +898,6 @@ def populate_global_pkgset(compose, koji_wrapper, path_prefix, event):
)
for variant in compose.all_variants.values():
if compose_tag in variant_tags[variant]:
# If it's a modular tag, store the package set for the module.
for nsvc, koji_tag in variant.module_uid_to_koji_tag.items():
if compose_tag == koji_tag:
@ -903,7 +920,7 @@ def populate_global_pkgset(compose, koji_wrapper, path_prefix, event):
MaterializedPackageSet.create,
compose,
pkgset,
path_prefix,
compose.koji_downloader.path_prefix,
mmd=tag_to_mmd.get(pkgset.name),
)
)

View File

@ -35,10 +35,12 @@ import pungi.wrappers.kojiwrapper
from pungi.wrappers.comps import CompsWrapper
from pungi.wrappers.mbs import MBSWrapper
import pungi.phases.pkgset.pkgsets
from pungi.util import (
retry,
get_arch_variant_data,
get_variant_data,
read_single_module_stream_from_string,
read_single_module_stream_from_file,
)
@ -101,12 +103,12 @@ def variant_dict_from_str(compose, module_str):
release_regex = re.compile(r"^(\d){14}$")
section_start = module_str.rfind("-")
module_str_first_part = module_str[section_start + 1 :]
module_str_first_part = module_str[section_start + 1:]
if release_regex.match(module_str_first_part):
module_info["version"] = module_str_first_part
module_str = module_str[:section_start]
section_start = module_str.rfind("-")
module_info["stream"] = module_str[section_start + 1 :]
module_info["stream"] = module_str[section_start + 1:]
else:
module_info["stream"] = module_str_first_part
module_info["name"] = module_str[:section_start]
@ -160,14 +162,16 @@ def get_koji_modules(compose, koji_wrapper, event, module_info_str):
# Store module versioning information into the dict, but make sure
# not to overwrite any existing keys.
md["module_stream"] = md["extra"]["typeinfo"]["module"]["stream"]
md["module_version"] = int(md["extra"]["typeinfo"]["module"]["version"])
md["module_version"] = int(
md["extra"]["typeinfo"]["module"]["version"])
md["module_context"] = md["extra"]["typeinfo"]["module"]["context"]
except KeyError:
continue
if md["state"] == pungi.wrappers.kojiwrapper.KOJI_BUILD_DELETED:
compose.log_debug(
"Module build %s has been deleted, ignoring it." % build["name"]
"Module build %s has been deleted, ignoring it." % build[
"name"]
)
continue
@ -189,7 +193,8 @@ def get_koji_modules(compose, koji_wrapper, event, module_info_str):
)
latest_version = sorted_modules[0]["module_version"]
modules = [
module for module in modules if latest_version == module["module_version"]
module for module in modules
if latest_version == module["module_version"]
]
return modules
@ -200,30 +205,32 @@ class PkgsetSourceKojiMock(pungi.phases.pkgset.source.PkgsetSourceBase):
def __call__(self):
compose = self.compose
koji_profile = compose.conf["koji_profile"]
self.koji_wrapper = pungi.wrappers.kojiwrapper.KojiMockWrapper(
koji_profile
compose,
get_all_arches(compose),
)
# path prefix must contain trailing '/'
path_prefix = self.koji_wrapper.koji_module.config.topdir.rstrip("/") + "/"
path_prefix = self.koji_wrapper.koji_module.config.topdir.rstrip(
"/") + "/"
package_sets = get_pkgset_from_koji(
self.compose, self.koji_wrapper, path_prefix
)
return (package_sets, path_prefix)
return package_sets, path_prefix
def get_pkgset_from_koji(compose, koji_wrapper, path_prefix):
event_info = get_koji_event_info(compose, koji_wrapper)
return populate_global_pkgset(compose, koji_wrapper, path_prefix, event_info)
return populate_global_pkgset(compose, koji_wrapper, path_prefix,
event_info)
def _add_module_to_variant(
koji_wrapper,
variant,
build,
add_to_variant_modules=False,
compose=None,
exclude_module_ns=None,
koji_wrapper,
variant,
build,
add_to_variant_modules=False,
compose=None,
exclude_module_ns=None,
):
"""
Adds module defined by Koji build info to variant.
@ -241,13 +248,16 @@ def _add_module_to_variant(
if archive["btype"] != "module":
# Skip non module archives
continue
filename = archive["filename"]
file_path = os.path.join(
koji_wrapper.koji_module.pathinfo.topdir,
'modules',
build['arch'],
build['extra']['typeinfo']['module']['content_koji_tag']
)
mmds[filename] = file_path
if len(mmds) <= 1:
@ -266,17 +276,22 @@ def _add_module_to_variant(
added = False
for arch in variant.arches:
if _is_filtered_out(compose, variant, arch, info["name"], info["stream"]):
compose.log_debug("Module %s is filtered from %s.%s", nsvc, variant, arch)
if _is_filtered_out(compose, variant, arch, info["name"],
info["stream"]):
compose.log_debug("Module %s is filtered from %s.%s", nsvc,
variant, arch)
continue
filename = "modulemd.%s.txt" % arch
try:
mod_stream = read_single_module_stream_from_file(
mmds[filename], compose, arch, build
)
if mod_stream:
added = True
variant.arch_mmds.setdefault(arch, {})[nsvc] = mod_stream
added = True
except KeyError:
# There is no modulemd for this arch. This could mean an arch was
@ -295,10 +310,11 @@ def _add_module_to_variant(
def _add_extra_modules_to_variant(
compose, koji_wrapper, variant, extra_modules, variant_tags, tag_to_mmd
compose, koji_wrapper, variant, extra_modules, variant_tags, tag_to_mmd
):
for nsvc in extra_modules:
msg = "Adding extra module build '%s' to variant '%s'" % (nsvc, variant)
msg = "Adding extra module build '%s' to variant '%s'" % (
nsvc, variant)
compose.log_info(msg)
nsvc_info = nsvc.split(":")
@ -341,10 +357,11 @@ def _add_extra_modules_to_variant(
def _add_scratch_modules_to_variant(
compose, variant, scratch_modules, variant_tags, tag_to_mmd
compose, variant, scratch_modules, variant_tags, tag_to_mmd
):
if compose.compose_type != "test" and scratch_modules:
compose.log_warning("Only test composes could include scratch module builds")
compose.log_warning(
"Only test composes could include scratch module builds")
return
mbs = MBSWrapper(compose.conf["mbs_api_url"])
@ -355,7 +372,8 @@ def _add_scratch_modules_to_variant(
try:
final_modulemd = mbs.final_modulemd(module_build["id"])
except Exception:
compose.log_error("Unable to get modulemd for build %s" % module_build)
compose.log_error(
"Unable to get modulemd for build %s" % module_build)
raise
tag = module_build["koji_tag"]
variant_tags[variant].append(tag)
@ -363,8 +381,7 @@ def _add_scratch_modules_to_variant(
for arch in variant.arches:
try:
mmd = read_single_module_stream_from_string(
final_modulemd[arch]
)
final_modulemd[arch])
variant.arch_mmds.setdefault(arch, {})[nsvc] = mmd
except KeyError:
continue
@ -390,21 +407,24 @@ def _is_filtered_out(compose, variant, arch, module_name, module_stream):
if not compose:
return False
for filter in get_arch_variant_data(compose.conf, "filter_modules", arch, variant):
for filter in get_arch_variant_data(compose.conf, "filter_modules", arch,
variant):
if ":" not in filter:
name_filter = filter
stream_filter = "*"
else:
name_filter, stream_filter = filter.split(":", 1)
if fnmatch(module_name, name_filter) and fnmatch(module_stream, stream_filter):
if fnmatch(module_name, name_filter) and fnmatch(module_stream,
stream_filter):
return True
return False
def _get_modules_from_koji(
compose, koji_wrapper, event, variant, variant_tags, tag_to_mmd
compose, koji_wrapper, event, variant, variant_tags, tag_to_mmd,
exclude_module_ns
):
"""
Loads modules for given `variant` from koji `session`, adds them to
@ -415,15 +435,21 @@ def _get_modules_from_koji(
:param Variant variant: Variant with modules to find.
:param dict variant_tags: Dict populated by this method. Key is `variant`
and value is list of Koji tags to get the RPMs from.
:param list exclude_module_ns: Module name:stream which will be excluded.
"""
# Find out all modules in every variant and add their Koji tags
# to variant and variant_tags list.
for module in variant.get_modules():
koji_modules = get_koji_modules(compose, koji_wrapper, event, module["name"])
koji_modules = get_koji_modules(compose, koji_wrapper, event,
module["name"])
for koji_module in koji_modules:
nsvc = _add_module_to_variant(
koji_wrapper, variant, koji_module, compose=compose
koji_wrapper,
variant,
koji_module,
compose=compose,
exclude_module_ns=exclude_module_ns,
)
if not nsvc:
continue
@ -462,7 +488,8 @@ def filter_inherited(koji_proxy, event, module_builds, top_tag):
does not understand streams, so we have to reimplement it here.
"""
inheritance = [
tag["name"] for tag in koji_proxy.getFullInheritance(top_tag, event=event["id"])
tag["name"] for tag in
koji_proxy.getFullInheritance(top_tag, event=event["id"])
]
def keyfunc(mb):
@ -487,7 +514,8 @@ def filter_inherited(koji_proxy, event, module_builds, top_tag):
return result
def filter_by_whitelist(compose, module_builds, input_modules, expected_modules):
def filter_by_whitelist(compose, module_builds, input_modules,
expected_modules):
"""
Exclude modules from the list that do not match any pattern specified in
input_modules. Order may not be preserved. The last argument is a set of
@ -511,6 +539,7 @@ def filter_by_whitelist(compose, module_builds, input_modules, expected_modules)
info.get("context"),
)
nvr_patterns.add((pattern, spec["name"]))
modules_to_keep = []
for mb in sorted(module_builds, key=lambda i: i['name']):
@ -521,13 +550,13 @@ def filter_by_whitelist(compose, module_builds, input_modules, expected_modules)
# and context.
for (n, s, v, c), spec in sorted(nvr_patterns):
if (
# We always have a name and stream...
mb["name"] == n
and mb["version"] == s
# ...but version and context can be missing, in which case we
# don't want to check them.
and (not v or ver == v)
and (not c or ctx == c)
# We always have a name and stream...
mb["name"] == n
and mb["version"] == s
# ...but version and context can be missing, in which case we
# don't want to check them.
and (not v or ver == v)
and (not c or ctx == c)
):
modules_to_keep.append(mb)
expected_modules.discard(spec)
@ -575,7 +604,13 @@ def _filter_expected_modules(
def _get_modules_from_koji_tags(
compose, koji_wrapper, event_id, variant, variant_tags, tag_to_mmd
compose,
koji_wrapper,
event_id,
variant,
variant_tags,
tag_to_mmd,
exclude_module_ns,
):
"""
Loads modules for given `variant` from Koji, adds them to
@ -587,10 +622,12 @@ def _get_modules_from_koji_tags(
:param Variant variant: Variant with modules to find.
:param dict variant_tags: Dict populated by this method. Key is `variant`
and value is list of Koji tags to get the RPMs from.
:param list exclude_module_ns: Module name:stream which will be excluded.
"""
# Compose tags from configuration
compose_tags = [
{"name": tag} for tag in force_list(compose.conf["pkgset_koji_module_tag"])
{"name": tag} for tag in
force_list(compose.conf["pkgset_koji_module_tag"])
]
# Get set of configured module names for this variant. If nothing is
# configured, the set is empty.
@ -617,7 +654,8 @@ def _get_modules_from_koji_tags(
)
# Filter out builds inherited from non-top tag
module_builds = filter_inherited(koji_proxy, event_id, module_builds, tag)
module_builds = filter_inherited(koji_proxy, event_id, module_builds,
tag)
# Apply whitelist of modules if specified.
variant_modules = variant.get_modules()
@ -646,10 +684,14 @@ def _get_modules_from_koji_tags(
latest_builds = []
module_builds = sorted(module_builds, key=_key, reverse=True)
for ns, ns_builds in groupby(
module_builds, key=lambda x: ":".join([x["name"], x["version"]])
module_builds, key=lambda x: ":".join([
x["name"],
x["version"],
x['arch'],
])
):
for nsv, nsv_builds in groupby(
ns_builds, key=lambda x: x["release"].split(".")[0]
ns_builds, key=lambda x: x["release"].split(".")[0]
):
latest_builds += list(nsv_builds)
break
@ -659,6 +701,18 @@ def _get_modules_from_koji_tags(
for build in latest_builds:
# Get the Build from Koji to get modulemd and module_tag.
build = koji_proxy.getBuild(build["build_id"])
nsvc = _add_module_to_variant(
koji_wrapper,
variant,
build,
True,
compose=compose,
exclude_module_ns=exclude_module_ns,
)
if not nsvc:
continue
module_tag = (
build.get("extra", {})
.get("typeinfo", {})
@ -668,12 +722,6 @@ def _get_modules_from_koji_tags(
variant_tags[variant].append(module_tag)
nsvc = _add_module_to_variant(
koji_wrapper, variant, build, True, compose=compose
)
if not nsvc:
continue
tag_to_mmd.setdefault(module_tag, {})
for arch in variant.arch_mmds:
try:
@ -705,8 +753,9 @@ def _get_modules_from_koji_tags(
# There are some module names that were listed in configuration and not
# found in any tag...
raise RuntimeError(
"Configuration specified patterns (%s) that don't match "
"any modules in the configured tags." % ", ".join(expected_modules)
f"Configuration specified patterns ({', '.join(expected_modules)})"
" that don't match any modules in "
f"the configured tags for variant '{variant.name}'"
)
@ -733,7 +782,7 @@ def populate_global_pkgset(compose, koji_wrapper, path_prefix, event):
for group in groups:
packages_to_gather += comps.get_packages(group)
if compose.conf["gather_method"] == "nodeps" and not compose.conf.get(
"buildinstall_method"
"buildinstall_method"
):
populate_only_packages_to_gather = True
else:
@ -764,26 +813,48 @@ def populate_global_pkgset(compose, koji_wrapper, path_prefix, event):
"modules."
)
extra_modules = get_variant_data(
compose.conf, "pkgset_koji_module_builds", variant
)
# When adding extra modules, other modules of the same name:stream available
# in brew tag should be excluded.
exclude_module_ns = []
if extra_modules:
exclude_module_ns = [
":".join(nsvc.split(":")[:2]) for nsvc in extra_modules
]
if modular_koji_tags or (
compose.conf["pkgset_koji_module_tag"] and variant.modules
compose.conf["pkgset_koji_module_tag"] and variant.modules
):
# List modules tagged in particular tags.
_get_modules_from_koji_tags(
compose, koji_wrapper, event, variant, variant_tags, tag_to_mmd
compose,
koji_wrapper,
event,
variant,
variant_tags,
tag_to_mmd,
exclude_module_ns,
)
elif variant.modules:
# Search each module in Koji separately. Tagging does not come into
# play here.
_get_modules_from_koji(
compose, koji_wrapper, event, variant, variant_tags, tag_to_mmd
compose,
koji_wrapper,
event,
variant,
variant_tags,
tag_to_mmd,
exclude_module_ns,
)
extra_modules = get_variant_data(
compose.conf, "pkgset_koji_module_builds", variant
)
if extra_modules:
_add_extra_modules_to_variant(
compose, koji_wrapper, variant, extra_modules, variant_tags, tag_to_mmd
compose, koji_wrapper, variant, extra_modules, variant_tags,
tag_to_mmd
)
variant_scratch_modules = get_variant_data(
@ -791,7 +862,8 @@ def populate_global_pkgset(compose, koji_wrapper, path_prefix, event):
)
if variant_scratch_modules:
_add_scratch_modules_to_variant(
compose, variant, variant_scratch_modules, variant_tags, tag_to_mmd
compose, variant, variant_scratch_modules, variant_tags,
tag_to_mmd
)
# Ensure that every tag added to `variant_tags` is added also to
@ -816,8 +888,10 @@ def populate_global_pkgset(compose, koji_wrapper, path_prefix, event):
for compose_tag in compose_tags:
compose.log_info("Loading package set for tag %s", compose_tag)
if compose_tag in pkgset_koji_tags:
extra_builds = force_list(compose.conf.get("pkgset_koji_builds", []))
extra_tasks = force_list(compose.conf.get("pkgset_koji_scratch_tasks", []))
extra_builds = force_list(
compose.conf.get("pkgset_koji_builds", []))
extra_tasks = force_list(
compose.conf.get("pkgset_koji_scratch_tasks", []))
else:
extra_builds = []
extra_tasks = []
@ -923,7 +997,8 @@ def populate_global_pkgset(compose, koji_wrapper, path_prefix, event):
def get_koji_event_info(compose, koji_wrapper):
event_file = os.path.join(compose.paths.work.topdir(arch="global"), "koji-event")
event_file = os.path.join(compose.paths.work.topdir(arch="global"),
"koji-event")
compose.log_info("Getting koji event")
result = get_koji_event_raw(koji_wrapper, compose.koji_event, event_file)

View File

@ -0,0 +1,63 @@
import argparse
import os
import re
import time
from pungi.util import format_size
LOCK_RE = re.compile(r".*\.lock(\|[A-Za-z0-9]+)*$")
def should_be_cleaned_up(path, st, threshold):
if st.st_nlink == 1 and st.st_mtime < threshold:
# No other instances, older than limit
return True
if LOCK_RE.match(path) and st.st_mtime < threshold:
# Suspiciously old lock
return True
return False
def main():
parser = argparse.ArgumentParser()
parser.add_argument("CACHE_DIR")
parser.add_argument("-n", "--dry-run", action="store_true")
parser.add_argument("--verbose", action="store_true")
parser.add_argument(
"--max-age",
help="how old files should be considered for deletion",
default=7,
type=int,
)
args = parser.parse_args()
topdir = os.path.abspath(args.CACHE_DIR)
max_age = args.max_age * 24 * 3600
cleaned_up = 0
threshold = time.time() - max_age
for dirpath, dirnames, filenames in os.walk(topdir):
for f in filenames:
filepath = os.path.join(dirpath, f)
st = os.stat(filepath)
if should_be_cleaned_up(filepath, st, threshold):
if args.verbose:
print("RM %s" % filepath)
cleaned_up += st.st_size
if not args.dry_run:
os.remove(filepath)
if not dirnames and not filenames:
if args.verbose:
print("RMDIR %s" % dirpath)
if not args.dry_run:
os.rmdir(dirpath)
if args.dry_run:
print("Would reclaim %s bytes." % format_size(cleaned_up))
else:
print("Reclaimed %s bytes." % format_size(cleaned_up))

View File

@ -171,32 +171,11 @@ def main():
group.add_argument(
"--offline", action="store_true", help="Do not resolve git references."
)
parser.add_argument(
"--multi",
metavar="DIR",
help=(
"Treat source as config for pungi-orchestrate and store dump into "
"given directory."
),
)
args = parser.parse_args()
defines = config_utils.extract_defines(args.define)
if args.multi:
if len(args.sources) > 1:
parser.error("Only one multi config can be specified.")
return dump_multi_config(
args.sources[0],
dest=args.multi,
defines=defines,
just_dump=args.just_dump,
event=args.freeze_event,
offline=args.offline,
)
return process_file(
args.sources,
defines=defines,

View File

@ -5,35 +5,43 @@ import os
import subprocess
import tempfile
from shutil import rmtree
from typing import AnyStr, List, Dict, Optional
from typing import (
AnyStr,
List,
Dict,
Optional,
)
import createrepo_c as cr
import requests
import yaml
from dataclasses import dataclass, field
from .create_packages_json import PackagesGenerator, RepoInfo
from .create_packages_json import (
PackagesGenerator,
RepoInfo,
VariantInfo,
)
@dataclass
class ExtraRepoInfo(RepoInfo):
class ExtraVariantInfo(VariantInfo):
modules: List[AnyStr] = field(default_factory=list)
packages: List[AnyStr] = field(default_factory=list)
is_remote: bool = True
class CreateExtraRepo(PackagesGenerator):
def __init__(
self,
repos: List[ExtraRepoInfo],
variants: List[ExtraVariantInfo],
bs_auth_token: AnyStr,
local_repository_path: AnyStr,
clear_target_repo: bool = True,
):
self.repos = [] # type: List[ExtraRepoInfo]
super().__init__(repos, [], [])
self.variants = [] # type: List[ExtraVariantInfo]
super().__init__(variants, [], [])
self.auth_headers = {
'Authorization': f'Bearer {bs_auth_token}',
}
@ -92,7 +100,7 @@ class CreateExtraRepo(PackagesGenerator):
arch: AnyStr,
packages: Optional[List[AnyStr]] = None,
modules: Optional[List[AnyStr]] = None,
) -> List[ExtraRepoInfo]:
) -> List[ExtraVariantInfo]:
"""
Get info about a BS repo and save it to
an object of class ExtraRepoInfo
@ -110,7 +118,7 @@ class CreateExtraRepo(PackagesGenerator):
api_uri = 'api/v1'
bs_repo_suffix = 'build_repos'
repos_info = []
variants_info = []
# get the full info about a BS repo
repo_request = requests.get(
@ -132,22 +140,26 @@ class CreateExtraRepo(PackagesGenerator):
# skip repo with unsuitable architecture
if architecture != arch:
continue
repo_info = ExtraRepoInfo(
path=os.path.join(
bs_url,
bs_repo_suffix,
build_id,
platform_name,
),
folder=architecture,
variant_info = ExtraVariantInfo(
name=f'{build_id}-{platform_name}-{architecture}',
arch=architecture,
is_remote=True,
packages=packages,
modules=modules,
repos=[
RepoInfo(
path=os.path.join(
bs_url,
bs_repo_suffix,
build_id,
platform_name,
),
folder=architecture,
is_remote=True,
)
]
)
repos_info.append(repo_info)
return repos_info
variants_info.append(variant_info)
return variants_info
def _create_local_extra_repo(self):
"""
@ -184,7 +196,7 @@ class CreateExtraRepo(PackagesGenerator):
def _download_rpm_to_local_repo(
self,
package_location: AnyStr,
repo_info: ExtraRepoInfo,
repo_info: RepoInfo,
) -> None:
"""
Download a rpm package from a remote repo and save it to a local repo
@ -212,37 +224,38 @@ class CreateExtraRepo(PackagesGenerator):
def _download_packages(
self,
packages: Dict[AnyStr, cr.Package],
repo_info: ExtraRepoInfo
variant_info: ExtraVariantInfo
):
"""
Download all defined packages from a remote repo
:param packages: information about all packages (including
modularity) in a remote repo
:param repo_info: information about a remote repo
:param variant_info: information about a remote variant
"""
for package in packages.values():
package_name = package.name
# Skip a current package from a remote repo if we defined
# the list packages and a current package doesn't belong to it
if repo_info.packages and \
package_name not in repo_info.packages:
if variant_info.packages and \
package_name not in variant_info.packages:
continue
self._download_rpm_to_local_repo(
package_location=package.location_href,
repo_info=repo_info,
)
for repo_info in variant_info.repos:
self._download_rpm_to_local_repo(
package_location=package.location_href,
repo_info=repo_info,
)
def _download_modules(
self,
modules_data: List[Dict],
repo_info: ExtraRepoInfo,
variant_info: ExtraVariantInfo,
packages: Dict[AnyStr, cr.Package]
):
"""
Download all defined modularity packages and their data from
a remote repo
:param modules_data: information about all modules in a remote repo
:param repo_info: information about a remote repo
:param variant_info: information about a remote variant
:param packages: information about all packages (including
modularity) in a remote repo
"""
@ -250,8 +263,8 @@ class CreateExtraRepo(PackagesGenerator):
module_data = module['data']
# Skip a current module from a remote repo if we defined
# the list modules and a current module doesn't belong to it
if repo_info.modules and \
module_data['name'] not in repo_info.modules:
if variant_info.modules and \
module_data['name'] not in variant_info.modules:
continue
# we should add info about a module if the local repodata
# doesn't have it
@ -266,15 +279,16 @@ class CreateExtraRepo(PackagesGenerator):
# Empty repo_info.packages means that we will download
# all packages from repo including
# the modularity packages
if not repo_info.packages:
if not variant_info.packages:
break
# skip a rpm if it doesn't belong to a processed repo
if rpm not in packages:
continue
self._download_rpm_to_local_repo(
package_location=packages[rpm].location_href,
repo_info=repo_info,
)
for repo_info in variant_info.repos:
self._download_rpm_to_local_repo(
package_location=packages[rpm].location_href,
repo_info=repo_info,
)
def create_extra_repo(self):
"""
@ -284,45 +298,34 @@ class CreateExtraRepo(PackagesGenerator):
3. Call `createrepo_c` which creates a local repo
with the right repodata
"""
for repo_info in self.repos:
packages = {} # type: Dict[AnyStr, cr.Package]
repomd_records = self._get_repomd_records(
repo_info=repo_info,
)
repomd_records_dict = {} # type: Dict[str, str]
self._download_repomd_records(
repo_info=repo_info,
repomd_records=repomd_records,
repomd_records_dict=repomd_records_dict,
)
packages_iterator = cr.PackageIterator(
primary_path=repomd_records_dict['primary'],
filelists_path=repomd_records_dict['filelists'],
other_path=repomd_records_dict['other'],
warningcb=self._warning_callback,
)
# parse the repodata (including modules.yaml.gz)
modules_data = self._parse_module_repomd_record(
repo_info=repo_info,
repomd_records=repomd_records,
)
# convert the packages dict to more usable form
# for future checking that a rpm from the module's artifacts
# belongs to a processed repository
packages = {
f'{package.name}-{package.epoch}:{package.version}-'
f'{package.release}.{package.arch}':
package for package in packages_iterator
}
self._download_modules(
modules_data=modules_data,
repo_info=repo_info,
packages=packages,
)
self._download_packages(
packages=packages,
repo_info=repo_info,
)
for variant_info in self.variants:
for repo_info in variant_info.repos:
repomd_records = self._get_repomd_records(
repo_info=repo_info,
)
packages_iterator = self.get_packages_iterator(repo_info)
# parse the repodata (including modules.yaml.gz)
modules_data = self._parse_module_repomd_record(
repo_info=repo_info,
repomd_records=repomd_records,
)
# convert the packages dict to more usable form
# for future checking that a rpm from the module's artifacts
# belongs to a processed repository
packages = {
f'{package.name}-{package.epoch}:{package.version}-'
f'{package.release}.{package.arch}':
package for package in packages_iterator
}
self._download_modules(
modules_data=modules_data,
variant_info=variant_info,
packages=packages,
)
self._download_packages(
packages=packages,
variant_info=variant_info,
)
self._dump_local_modules_yaml()
self._create_local_extra_repo()
@ -333,7 +336,6 @@ def create_parser():
parser.add_argument(
'--bs-auth-token',
help='Auth token for Build System',
required=True,
)
parser.add_argument(
'--local-repo-path',
@ -402,11 +404,16 @@ def cli_main():
packages = packages.split()
if repo.startswith('http://'):
repos_info.append(
ExtraRepoInfo(
path=repo,
folder=repo_folder,
ExtraVariantInfo(
name=repo_folder,
arch=repo_arch,
repos=[
RepoInfo(
path=repo,
folder=repo_folder,
is_remote=True,
)
],
modules=modules,
packages=packages,
)
@ -422,7 +429,7 @@ def cli_main():
)
)
cer = CreateExtraRepo(
repos=repos_info,
variants=repos_info,
bs_auth_token=args.bs_auth_token,
local_repository_path=args.local_repo_path,
clear_target_repo=args.clear_local_repo,

View File

@ -9,22 +9,41 @@ https://github.com/rpm-software-management/createrepo_c/blob/master/examples/pyt
import argparse
import gzip
import json
import logging
import lzma
import os
import re
import tempfile
from collections import defaultdict
from typing import AnyStr, Dict, List, Optional, Any, Iterator
from itertools import tee
from pathlib import Path
from typing import (
AnyStr,
Dict,
List,
Any,
Iterator,
Optional,
Tuple,
Union,
)
import binascii
import createrepo_c as cr
import dnf.subject
import hawkey
from urllib.parse import urljoin
import requests
import rpm
import yaml
from createrepo_c import Package, PackageIterator
from dataclasses import dataclass
from createrepo_c import (
Package,
PackageIterator,
Repomd,
RepomdRecord,
)
from dataclasses import dataclass, field
from kobo.rpmlib import parse_nvra
logging.basicConfig(level=logging.INFO)
def _is_compressed_file(first_two_bytes: bytes, initial_bytes: bytes):
@ -51,38 +70,76 @@ class RepoInfo:
# 'appstream', 'baseos', etc.
# Or 'http://koji.cloudlinux.com/mirrors/rhel_mirror' if you are
# using remote repo
path: AnyStr
path: str
# name of folder with a repodata folder. E.g. 'baseos', 'appstream', etc
folder: AnyStr
# name of repo. E.g. 'BaseOS', 'AppStream', etc
name: AnyStr
# architecture of repo. E.g. 'x86_64', 'i686', etc
arch: AnyStr
folder: str
# Is a repo remote or local
is_remote: bool
# Is a reference repository (usually it's a RHEL repo)
# Layout of packages from such repository will be taken as example
# Only layout of specific package (which don't exist
# Only layout of specific package (which doesn't exist
# in a reference repository) will be taken as example
is_reference: bool = False
# The packages from 'present' repo will be added to a variant.
# The packages from 'absent' repo will be removed from a variant.
repo_type: str = 'present'
@dataclass
class VariantInfo:
# name of variant. E.g. 'BaseOS', 'AppStream', etc
name: AnyStr
# architecture of variant. E.g. 'x86_64', 'i686', etc
arch: AnyStr
# The packages which will be not added to a variant
excluded_packages: List[str] = field(default_factory=list)
# Repos of a variant
repos: List[RepoInfo] = field(default_factory=list)
class PackagesGenerator:
repo_arches = defaultdict(lambda: list(('noarch',)))
addon_repos = {
'x86_64': ['i686'],
'ppc64le': [],
'aarch64': [],
's390x': [],
'i686': [],
}
def __init__(
self,
repos: List[RepoInfo],
variants: List[VariantInfo],
excluded_packages: List[AnyStr],
included_packages: List[AnyStr],
):
self.repos = repos
self.variants = variants
self.pkgs = dict()
self.excluded_packages = excluded_packages
self.included_packages = included_packages
self.tmp_files = []
self.tmp_files = [] # type: list[Path]
for arch, arch_list in self.addon_repos.items():
self.repo_arches[arch].extend(arch_list)
self.repo_arches[arch].append(arch)
def __del__(self):
for tmp_file in self.tmp_files:
if os.path.exists(tmp_file):
os.remove(tmp_file)
if tmp_file.exists():
tmp_file.unlink()
@staticmethod
def _get_full_repo_path(repo_info: RepoInfo):
result = os.path.join(
repo_info.path,
repo_info.folder
)
if repo_info.is_remote:
result = urljoin(
repo_info.path + '/',
repo_info.folder,
)
return result
@staticmethod
def _warning_callback(warning_type, message):
@ -92,8 +149,7 @@ class PackagesGenerator:
print(f'Warning message: "{message}"; warning type: "{warning_type}"')
return True
@staticmethod
def get_remote_file_content(file_url: AnyStr) -> AnyStr:
def get_remote_file_content(self, file_url: AnyStr) -> AnyStr:
"""
Get content from a remote file and write it to a temp file
:param file_url: url of a remote file
@ -106,78 +162,16 @@ class PackagesGenerator:
file_request.raise_for_status()
with tempfile.NamedTemporaryFile(delete=False) as file_stream:
file_stream.write(file_request.content)
self.tmp_files.append(Path(file_stream.name))
return file_stream.name
@staticmethod
def _parse_repomd(repomd_file_path: AnyStr) -> cr.Repomd:
def _parse_repomd(repomd_file_path: AnyStr) -> Repomd:
"""
Parse file repomd.xml and create object Repomd
:param repomd_file_path: path to local repomd.xml
"""
return cr.Repomd(repomd_file_path)
def _parse_primary_file(
self,
primary_file_path: AnyStr,
packages: Dict[AnyStr, cr.Package],
) -> None:
"""
Parse primary.xml.gz, take from it info about packages and put it to
dict packages
:param primary_file_path: path to local primary.xml.gz
:param packages: dictionary which will contain info about packages
from repository
"""
cr.xml_parse_primary(
path=primary_file_path,
pkgcb=lambda pkg: packages.update({
pkg.pkgId: pkg,
}),
do_files=False,
warningcb=self._warning_callback,
)
def _parse_filelists_file(
self,
filelists_file_path: AnyStr,
packages: Dict[AnyStr, cr.Package],
) -> None:
"""
Parse filelists.xml.gz, take from it info about packages and put it to
dict packages
:param filelists_file_path: path to local filelists.xml.gz
:param packages: dictionary which will contain info about packages
from repository
"""
cr.xml_parse_filelists(
path=filelists_file_path,
newpkgcb=lambda pkg_id, name, arch: packages.get(
pkg_id,
None,
),
warningcb=self._warning_callback,
)
def _parse_other_file(
self,
other_file_path: AnyStr,
packages: Dict[AnyStr, cr.Package],
) -> None:
"""
Parse other.xml.gz, take from it info about packages and put it to
dict packages
:param other_file_path: path to local other.xml.gz
:param packages: dictionary which will contain info about packages
from repository
"""
cr.xml_parse_other(
path=other_file_path,
newpkgcb=lambda pkg_id, name, arch: packages.get(
pkg_id,
None,
),
warningcb=self._warning_callback,
)
return Repomd(repomd_file_path)
@classmethod
def _parse_modules_file(
@ -188,7 +182,7 @@ class PackagesGenerator:
"""
Parse modules.yaml.gz and returns parsed data
:param modules_file_path: path to local modules.yaml.gz
:return: List of dict for each modules in a repo
:return: List of dict for each module in a repo
"""
with open(modules_file_path, 'rb') as modules_file:
@ -205,7 +199,7 @@ class PackagesGenerator:
def _get_repomd_records(
self,
repo_info: RepoInfo,
) -> List[cr.RepomdRecord]:
) -> List[RepomdRecord]:
"""
Get, parse file repomd.xml and extract from it repomd records
:param repo_info: structure which contains info about a current repo
@ -218,9 +212,15 @@ class PackagesGenerator:
'repomd.xml',
)
if repo_info.is_remote:
repomd_file_path = urljoin(
urljoin(
repo_info.path + '/',
repo_info.folder
) + '/',
'repodata/repomd.xml'
)
repomd_file_path = self.get_remote_file_content(repomd_file_path)
else:
repomd_file_path = repomd_file_path
repomd_object = self._parse_repomd(repomd_file_path)
if repo_info.is_remote:
os.remove(repomd_file_path)
@ -229,7 +229,7 @@ class PackagesGenerator:
def _download_repomd_records(
self,
repo_info: RepoInfo,
repomd_records: List[cr.RepomdRecord],
repomd_records: List[RepomdRecord],
repomd_records_dict: Dict[str, str],
):
"""
@ -253,19 +253,17 @@ class PackagesGenerator:
if repo_info.is_remote:
repomd_record_file_path = self.get_remote_file_content(
repomd_record_file_path)
self.tmp_files.append(repomd_record_file_path)
repomd_records_dict[repomd_record.type] = repomd_record_file_path
def _parse_module_repomd_record(
self,
repo_info: RepoInfo,
repomd_records: List[cr.RepomdRecord],
repomd_records: List[RepomdRecord],
) -> List[Dict]:
"""
Download repomd records
:param repo_info: structure which contains info about a current repo
:param repomd_records: list with repomd records
:param repomd_records_dict: dict with paths to repodata files
"""
for repomd_record in repomd_records:
if repomd_record.type != 'modules':
@ -278,10 +276,10 @@ class PackagesGenerator:
if repo_info.is_remote:
repomd_record_file_path = self.get_remote_file_content(
repomd_record_file_path)
self.tmp_files.append(repomd_record_file_path)
return list(self._parse_modules_file(
repomd_record_file_path,
))
return []
@staticmethod
def compare_pkgs_version(package_1: Package, package_2: Package) -> int:
@ -297,30 +295,13 @@ class PackagesGenerator:
)
return rpm.labelCompare(version_tuple_1, version_tuple_2)
def generate_packages_json(
self
) -> Dict[AnyStr, Dict[AnyStr, Dict[AnyStr, List[AnyStr]]]]:
"""
Generate packages.json
"""
packages_json = defaultdict(
lambda: defaultdict(
lambda: defaultdict(
list,
)
)
)
all_packages = defaultdict(lambda: {'variants': list()})
for repo_info in self.repos:
repo_arches = [
repo_info.arch,
'noarch',
]
if repo_info.arch == 'x86_64':
repo_arches.extend([
'i686',
'i386',
])
def get_packages_iterator(
self,
repo_info: RepoInfo,
) -> Union[PackageIterator, Iterator]:
full_repo_path = self._get_full_repo_path(repo_info)
pkgs_iterator = self.pkgs.get(full_repo_path)
if pkgs_iterator is None:
repomd_records = self._get_repomd_records(
repo_info=repo_info,
)
@ -330,156 +311,146 @@ class PackagesGenerator:
repomd_records=repomd_records,
repomd_records_dict=repomd_records_dict,
)
packages_iterator = PackageIterator(
pkgs_iterator = PackageIterator(
primary_path=repomd_records_dict['primary'],
filelists_path=repomd_records_dict['filelists'],
other_path=repomd_records_dict['other'],
warningcb=self._warning_callback,
)
for package in packages_iterator:
if package.arch not in repo_arches:
package_arch = repo_info.arch
else:
package_arch = package.arch
package_key = f'{package.name}.{package_arch}'
if 'module' in package.release and not any(
re.search(included_package, package.name)
for included_package in self.included_packages
):
# Even a module package will be added to packages.json if
# it presents in the list of included packages
continue
if package_key not in all_packages:
all_packages[package_key]['variants'].append(
repo_info.name
)
all_packages[package_key]['arch'] = repo_info.arch
all_packages[package_key]['package'] = package
all_packages[package_key]['type'] = repo_info.is_reference
# replace an older package if it's not reference or
# a newer package is from reference repo
elif (not all_packages[package_key]['type'] or
all_packages[package_key]['type'] ==
repo_info.is_reference) and \
self.compare_pkgs_version(
package,
all_packages[package_key]['package']
) > 0:
all_packages[package_key]['variants'] = [repo_info.name]
all_packages[package_key]['arch'] = repo_info.arch
all_packages[package_key]['package'] = package
elif self.compare_pkgs_version(
package,
all_packages[package_key]['package']
) == 0:
all_packages[package_key]['variants'].append(
repo_info.name
)
pkgs_iterator, self.pkgs[full_repo_path] = tee(pkgs_iterator)
return pkgs_iterator
for package_dict in all_packages.values():
repo_arches = [
package_dict['arch'],
'noarch',
]
if package_dict['arch'] == 'x86_64':
repo_arches.extend([
'i686',
'i386',
])
for variant in package_dict['variants']:
repo_arch = package_dict['arch']
package = package_dict['package']
package_name = package.name
if package.arch not in repo_arches:
package_arch = package_dict['arch']
else:
package_arch = package.arch
if any(re.search(excluded_package, package_name)
for excluded_package in self.excluded_packages):
continue
src_package_name = dnf.subject.Subject(
package.rpm_sourcerpm,
).get_nevra_possibilities(
forms=hawkey.FORM_NEVRA,
)
if len(src_package_name) > 1:
# We should stop utility if we can't get exact name of srpm
raise ValueError(
'We can\'t get exact name of srpm '
f'by its NEVRA "{package.rpm_sourcerpm}"'
def get_package_arch(
self,
package: Package,
variant_arch: str,
) -> str:
result = variant_arch
if package.arch in self.repo_arches[variant_arch]:
result = package.arch
return result
def is_skipped_module_package(
self,
package: Package,
variant_arch: str,
) -> bool:
package_key = self.get_package_key(package, variant_arch)
# Even a module package will be added to packages.json if
# it presents in the list of included packages
return 'module' in package.release and not any(
re.search(
f'^{included_pkg}$',
package_key,
) or included_pkg in (package.name, package_key)
for included_pkg in self.included_packages
)
def is_excluded_package(
self,
package: Package,
variant_arch: str,
excluded_packages: List[str],
) -> bool:
package_key = self.get_package_key(package, variant_arch)
return any(
re.search(
f'^{excluded_pkg}$',
package_key,
) or excluded_pkg in (package.name, package_key)
for excluded_pkg in excluded_packages
)
@staticmethod
def get_source_rpm_name(package: Package) -> str:
source_rpm_nvra = parse_nvra(package.rpm_sourcerpm)
return source_rpm_nvra['name']
def get_package_key(self, package: Package, variant_arch: str) -> str:
return (
f'{package.name}.'
f'{self.get_package_arch(package, variant_arch)}'
)
def generate_packages_json(
self
) -> Dict[AnyStr, Dict[AnyStr, Dict[AnyStr, List[AnyStr]]]]:
"""
Generate packages.json
"""
packages = defaultdict(lambda: defaultdict(lambda: {
'variants': list(),
}))
for variant_info in self.variants:
for repo_info in variant_info.repos:
is_reference = repo_info.is_reference
for package in self.get_packages_iterator(repo_info=repo_info):
if self.is_skipped_module_package(
package=package,
variant_arch=variant_info.arch,
):
continue
if self.is_excluded_package(
package=package,
variant_arch=variant_info.arch,
excluded_packages=self.excluded_packages,
):
continue
if self.is_excluded_package(
package=package,
variant_arch=variant_info.arch,
excluded_packages=variant_info.excluded_packages,
):
continue
package_key = self.get_package_key(
package,
variant_info.arch,
)
else:
src_package_name = src_package_name[0].name
pkgs_list = packages_json[variant][
repo_arch][src_package_name]
added_pkg = f'{package_name}.{package_arch}'
if added_pkg not in pkgs_list:
pkgs_list.append(added_pkg)
return packages_json
source_rpm_name = self.get_source_rpm_name(package)
package_info = packages[source_rpm_name][package_key]
if 'is_reference' not in package_info:
package_info['variants'].append(variant_info.name)
package_info['is_reference'] = is_reference
package_info['package'] = package
elif not package_info['is_reference'] or \
package_info['is_reference'] == is_reference and \
self.compare_pkgs_version(
package_1=package,
package_2=package_info['package'],
) > 0:
package_info['variants'] = [variant_info.name]
package_info['is_reference'] = is_reference
package_info['package'] = package
elif self.compare_pkgs_version(
package_1=package,
package_2=package_info['package'],
) == 0 and repo_info.repo_type != 'absent':
package_info['variants'].append(variant_info.name)
result = defaultdict(lambda: defaultdict(
lambda: defaultdict(list),
))
for variant_info in self.variants:
for source_rpm_name, packages_info in packages.items():
for package_key, package_info in packages_info.items():
variant_pkgs = result[variant_info.name][variant_info.arch]
if variant_info.name not in package_info['variants']:
continue
variant_pkgs[source_rpm_name].append(package_key)
return result
def create_parser():
parser = argparse.ArgumentParser()
parser.add_argument(
'--repo-path',
action='append',
help='Path to a folder with repofolders. E.g. "/var/repos" or '
'"http://koji.cloudlinux.com/mirrors/rhel_mirror"',
required=True,
)
parser.add_argument(
'--repo-folder',
action='append',
help='A folder which contains folder repodata . E.g. "baseos-stream"',
required=True,
)
parser.add_argument(
'--repo-arch',
action='append',
help='What architecture packages a repository contains. E.g. "x86_64"',
required=True,
)
parser.add_argument(
'--repo-name',
action='append',
help='Name of a repository. E.g. "AppStream"',
required=True,
)
parser.add_argument(
'--is-remote',
action='append',
type=str,
help='A repository is remote or local',
choices=['yes', 'no'],
required=True,
)
parser.add_argument(
'--is-reference',
action='append',
type=str,
help='A repository is used as reference for packages layout',
choices=['yes', 'no'],
required=True,
)
parser.add_argument(
'--excluded-packages',
nargs='+',
type=str,
default=[],
help='A list of globally excluded packages from generated json.'
'All of list elements should be separated by space',
required=False,
)
parser.add_argument(
'--included-packages',
nargs='+',
type=str,
default=[],
help='A list of globally included packages from generated json.'
'All of list elements should be separated by space',
'-c',
'--config',
type=Path,
default=Path('config.yaml'),
required=False,
help='Path to a config',
)
parser.add_argument(
'-o',
'--json-output-path',
type=str,
help='Full path to output json file',
@ -489,30 +460,45 @@ def create_parser():
return parser
def read_config(config_path: Path) -> Optional[Dict]:
if not config_path.exists():
logging.error('A config by path "%s" does not exist', config_path)
exit(1)
with config_path.open('r') as config_fd:
return yaml.safe_load(config_fd)
def process_config(config_data: Dict) -> Tuple[
List[VariantInfo],
List[str],
List[str],
]:
excluded_packages = config_data.get('excluded_packages', [])
included_packages = config_data.get('included_packages', [])
variants = [VariantInfo(
name=variant_name,
arch=variant_info['arch'],
excluded_packages=variant_info.get('excluded_packages', []),
repos=[RepoInfo(
path=variant_repo['path'],
folder=variant_repo['folder'],
is_remote=variant_repo['remote'],
is_reference=variant_repo['reference'],
repo_type=variant_repo.get('repo_type', 'present'),
) for variant_repo in variant_info['repos']]
) for variant_name, variant_info in config_data['variants'].items()]
return variants, excluded_packages, included_packages
def cli_main():
args = create_parser().parse_args()
repos = []
for repo_path, repo_folder, repo_name, \
repo_arch, is_remote, is_reference in zip(
args.repo_path,
args.repo_folder,
args.repo_name,
args.repo_arch,
args.is_remote,
args.is_reference,
):
repos.append(RepoInfo(
path=repo_path,
folder=repo_folder,
name=repo_name,
arch=repo_arch,
is_remote=True if is_remote == 'yes' else False,
is_reference=True if is_reference == 'yes' else False
))
variants, excluded_packages, included_packages = process_config(
config_data=read_config(args.config)
)
pg = PackagesGenerator(
repos=repos,
excluded_packages=args.excluded_packages,
included_packages=args.included_packages,
variants=variants,
excluded_packages=excluded_packages,
included_packages=included_packages,
)
result = pg.generate_packages_json()
with open(args.json_output_path, 'w') as packages_file:

View File

@ -14,6 +14,9 @@ def send(cmd, data):
topic = "compose.%s" % cmd.replace("-", ".").lower()
try:
msg = fedora_messaging.api.Message(topic="pungi.{}".format(topic), body=data)
if cmd == "ostree":
# https://pagure.io/fedora-infrastructure/issue/10899
msg.priority = 3
fedora_messaging.api.publish(msg)
except fedora_messaging.exceptions.PublishReturned as e:
print("Fedora Messaging broker rejected message %s: %s" % (msg.id, e))

View File

@ -2,6 +2,7 @@ import gzip
import lzma
import os
from argparse import ArgumentParser, FileType
from glob import iglob
from io import BytesIO
from pathlib import Path
from typing import List, AnyStr, Iterable, Union, Optional
@ -30,8 +31,11 @@ def grep_list_of_modules_yaml(repos_path: AnyStr) -> Iterable[BytesIO]:
"""
return (
read_modules_yaml_from_specific_repo(repo_path=path.parent)
for path in Path(repos_path).rglob('repodata')
read_modules_yaml_from_specific_repo(repo_path=Path(path).parent)
for path in iglob(
str(Path(repos_path).joinpath('**/repodata')),
recursive=True
)
)
@ -55,7 +59,12 @@ def read_modules_yaml_from_specific_repo(
repo_path + '/',
'repodata/repomd.xml',
)
repomd_file_path = PackagesGenerator.get_remote_file_content(
packages_generator = PackagesGenerator(
variants=[],
excluded_packages=[],
included_packages=[],
)
repomd_file_path = packages_generator.get_remote_file_content(
file_url=repomd_url
)
else:

View File

@ -1,39 +1,53 @@
import re
from argparse import ArgumentParser
import os
from glob import iglob
from typing import List
from pathlib import Path
from attr import dataclass
from dataclasses import dataclass
from productmd.common import parse_nvra
@dataclass
class Package:
nvra: str
path: str
nvra: dict
path: Path
def search_rpms(top_dir) -> List[Package]:
def search_rpms(top_dir: Path) -> List[Package]:
"""
Search for all *.rpm files recursively
in given top directory
Returns:
list: list of paths
"""
rpms = []
for root, dirs, files in os.walk(top_dir):
path = root.split(os.sep)
for file in files:
if not file.endswith('.rpm'):
continue
nvra, _ = os.path.splitext(file)
rpms.append(
Package(nvra=nvra, path=os.path.join('/', *path, file))
)
return rpms
return [Package(
nvra=parse_nvra(Path(path).stem),
path=Path(path),
) for path in iglob(str(top_dir.joinpath('**/*.rpm')), recursive=True)]
def copy_rpms(packages: List[Package], target_top_dir: str):
def is_excluded_package(
package: Package,
excluded_packages: List[str],
) -> bool:
package_key = f'{package.nvra["name"]}.{package.nvra["arch"]}'
return any(
re.search(
f'^{excluded_pkg}$',
package_key,
) or excluded_pkg in (package.nvra['name'], package_key)
for excluded_pkg in excluded_packages
)
def copy_rpms(
packages: List[Package],
target_top_dir: Path,
excluded_packages: List[str],
):
"""
Search synced repos for rpms and prepare
koji-like structure for pungi
@ -45,30 +59,37 @@ def copy_rpms(packages: List[Package], target_top_dir: str):
Nothing:
"""
for package in packages:
info = parse_nvra(package.nvra)
target_arch_dir = os.path.join(target_top_dir, info['arch'])
if is_excluded_package(package, excluded_packages):
continue
target_arch_dir = target_top_dir.joinpath(package.nvra['arch'])
target_file = target_arch_dir.joinpath(package.path.name)
os.makedirs(target_arch_dir, exist_ok=True)
target_file = os.path.join(target_arch_dir, os.path.basename(package.path))
if not os.path.exists(target_file):
if not target_file.exists():
try:
os.link(package.path, target_file)
except OSError:
# hardlink failed, try symlinking
os.symlink(package.path, target_file)
package.path.symlink_to(target_file)
def cli_main():
parser = ArgumentParser()
parser.add_argument('-p', '--path', required=True)
parser.add_argument('-t', '--target', required=True)
parser.add_argument('-p', '--path', required=True, type=Path)
parser.add_argument('-t', '--target', required=True, type=Path)
parser.add_argument(
'-e',
'--excluded-packages',
required=False,
nargs='+',
type=str,
default=[],
)
namespace = parser.parse_args()
rpms = search_rpms(namespace.path)
copy_rpms(rpms, namespace.target)
copy_rpms(rpms, namespace.target, namespace.excluded_packages)
if __name__ == '__main__':

View File

@ -319,7 +319,6 @@ def get_arguments(config):
def main():
config = pungi.config.Config()
opts = get_arguments(config)

View File

@ -23,6 +23,7 @@ from pungi.phases import PHASES_NAMES
from pungi import get_full_version, util
from pungi.errors import UnsignedPackagesError
from pungi.wrappers import kojiwrapper
from pungi.util import rmtree
# force C locales
@ -251,9 +252,15 @@ def main():
kobo.log.add_stderr_logger(logger)
conf = util.load_config(opts.config)
compose_type = opts.compose_type or conf.get("compose_type", "production")
if compose_type == "production" and not opts.label and not opts.no_label:
label = opts.label or conf.get("label")
if label:
try:
productmd.composeinfo.verify_label(label)
except ValueError as ex:
abort(str(ex))
if compose_type == "production" and not label and not opts.no_label:
abort("must specify label for a production compose")
if (
@ -300,7 +307,12 @@ def main():
if opts.target_dir:
compose_dir = Compose.get_compose_dir(
opts.target_dir, conf, compose_type=compose_type, compose_label=opts.label
opts.target_dir,
conf,
compose_type=compose_type,
compose_label=label,
parent_compose_ids=opts.parent_compose_id,
respin_of=opts.respin_of,
)
else:
compose_dir = opts.compose_dir
@ -309,7 +321,7 @@ def main():
ci = Compose.get_compose_info(
conf,
compose_type=compose_type,
compose_label=opts.label,
compose_label=label,
parent_compose_ids=opts.parent_compose_id,
respin_of=opts.respin_of,
)
@ -380,6 +392,14 @@ def run_compose(
compose.log_info("Current timezone offset: %s" % pungi.util.get_tz_offset())
compose.log_info("COMPOSE_ID=%s" % compose.compose_id)
installed_pkgs_log = compose.paths.log.log_file("global", "installed-pkgs")
compose.log_info("Logging installed packages to %s" % installed_pkgs_log)
try:
with open(installed_pkgs_log, "w") as f:
subprocess.Popen(["rpm", "-qa"], stdout=f)
except Exception as e:
compose.log_warning("Failed to log installed packages: %s" % str(e))
compose.read_variants()
# dump the config file
@ -671,7 +691,7 @@ def cli_main():
except (Exception, KeyboardInterrupt) as ex:
if COMPOSE:
COMPOSE.log_error("Compose run failed: %s" % ex)
COMPOSE.traceback()
COMPOSE.traceback(show_locals=getattr(ex, "show_locals", True))
COMPOSE.log_critical("Compose failed: %s" % COMPOSE.topdir)
COMPOSE.write_status("DOOMED")
else:
@ -680,3 +700,8 @@ def cli_main():
sys.stdout.flush()
sys.stderr.flush()
sys.exit(1)
finally:
# Remove repositories cloned during ExtraFiles phase
process_id = os.getpid()
directoy_to_remove = "/tmp/pungi-temp-git-repos-" + str(process_id) + "/"
rmtree(directoy_to_remove)

View File

@ -279,7 +279,7 @@ class GitUrlResolveError(RuntimeError):
pass
def resolve_git_ref(repourl, ref):
def resolve_git_ref(repourl, ref, credential_helper=None):
"""Resolve a reference in a Git repo to a commit.
Raises RuntimeError if there was an error. Most likely cause is failure to
@ -289,7 +289,7 @@ def resolve_git_ref(repourl, ref):
# This looks like a commit ID already.
return ref
try:
_, output = git_ls_remote(repourl, ref)
_, output = git_ls_remote(repourl, ref, credential_helper)
except RuntimeError as e:
raise GitUrlResolveError(
"ref does not exist in remote repo %s with the error %s %s"
@ -316,7 +316,7 @@ def resolve_git_ref(repourl, ref):
return lines[0].split()[0]
def resolve_git_url(url):
def resolve_git_url(url, credential_helper=None):
"""Given a url to a Git repo specifying HEAD or origin/<branch> as a ref,
replace that specifier with actual SHA1 of the commit.
@ -335,7 +335,7 @@ def resolve_git_url(url):
scheme = r.scheme.replace("git+", "")
baseurl = urllib.parse.urlunsplit((scheme, r.netloc, r.path, "", ""))
fragment = resolve_git_ref(baseurl, ref)
fragment = resolve_git_ref(baseurl, ref, credential_helper)
result = urllib.parse.urlunsplit((r.scheme, r.netloc, r.path, r.query, fragment))
if "?#" in url:
@ -354,13 +354,18 @@ class GitUrlResolver(object):
self.offline = offline
self.cache = {}
def __call__(self, url, branch=None):
def __call__(self, url, branch=None, options=None):
credential_helper = options.get("credential_helper") if options else None
if self.offline:
return branch or url
key = (url, branch)
if key not in self.cache:
try:
res = resolve_git_ref(url, branch) if branch else resolve_git_url(url)
res = (
resolve_git_ref(url, branch, credential_helper)
if branch
else resolve_git_url(url, credential_helper)
)
self.cache[key] = res
except GitUrlResolveError as exc:
self.cache[key] = exc
@ -456,6 +461,9 @@ def get_volid(compose, arch, variant=None, disc_type=False, formats=None, **kwar
if not variant_uid and "%(variant)s" in i:
continue
try:
# fmt: off
# Black wants to add a comma after kwargs, but that's not valid in
# Python 2.7
args = get_format_substs(
compose,
variant=variant_uid,
@ -467,6 +475,7 @@ def get_volid(compose, arch, variant=None, disc_type=False, formats=None, **kwar
base_product_version=base_product_version,
**kwargs
)
# fmt: on
volid = (i % args).format(**args)
except KeyError as err:
raise RuntimeError(
@ -991,8 +1000,12 @@ def retry(timeout=120, interval=30, wait_on=Exception):
@retry(wait_on=RuntimeError)
def git_ls_remote(baseurl, ref):
return run(["git", "ls-remote", baseurl, ref], universal_newlines=True)
def git_ls_remote(baseurl, ref, credential_helper=None):
cmd = ["git"]
if credential_helper:
cmd.extend(["-c", "credential.useHttpPath=true"])
cmd.extend(["-c", "credential.helper=%s" % credential_helper])
return run(cmd + ["ls-remote", baseurl, ref], universal_newlines=True)
def get_tz_offset():
@ -1137,3 +1150,16 @@ def read_json_file(file_path):
"""A helper function to read a JSON file."""
with open(file_path) as f:
return json.load(f)
UNITS = ["", "Ki", "Mi", "Gi", "Ti"]
def format_size(sz):
sz = float(sz)
unit = 0
while sz > 1024:
sz /= 1024
unit += 1
return "%.3g %sB" % (sz, UNITS[unit])

View File

@ -183,15 +183,16 @@ class CompsFilter(object):
"""
all_groups = self.tree.xpath("/comps/group/id/text()") + lookaside_groups
for environment in self.tree.xpath("/comps/environment"):
for group in environment.xpath("grouplist/groupid"):
if group.text not in all_groups:
group.getparent().remove(group)
for parent_tag in ("grouplist", "optionlist"):
for group in environment.xpath("%s/groupid" % parent_tag):
if group.text not in all_groups:
group.getparent().remove(group)
for group in environment.xpath("grouplist/groupid[@arch]"):
value = group.attrib.get("arch")
values = [v for v in re.split(r"[, ]+", value) if v]
if arch not in values:
group.getparent().remove(group)
for group in environment.xpath("%s/groupid[@arch]" % parent_tag):
value = group.attrib.get("arch")
values = [v for v in re.split(r"[, ]+", value) if v]
if arch not in values:
group.getparent().remove(group)
def remove_empty_environments(self):
"""

View File

@ -260,20 +260,23 @@ def get_isohybrid_cmd(iso_path, arch):
return cmd
def get_manifest_cmd(iso_name, xorriso=False):
def get_manifest_cmd(iso_name, xorriso=False, output_file=None):
if not output_file:
output_file = "%s.manifest" % iso_name
if xorriso:
return """xorriso -dev %s --find |
tail -n+2 |
tr -d "'" |
cut -c2- |
sort >> %s.manifest""" % (
shlex_quote(iso_name),
sort >> %s""" % (
shlex_quote(iso_name),
shlex_quote(output_file),
)
else:
return "isoinfo -R -f -i %s | grep -v '/TRANS.TBL$' | sort >> %s.manifest" % (
shlex_quote(iso_name),
return "isoinfo -R -f -i %s | grep -v '/TRANS.TBL$' | sort >> %s" % (
shlex_quote(iso_name),
shlex_quote(output_file),
)

View File

@ -43,10 +43,11 @@ class KojiMock:
Class that acts like real koji (for some needed methods)
but uses local storage as data source
"""
def __init__(self, packages_dir, modules_dir):
def __init__(self, packages_dir, modules_dir, all_arches):
self._modules = self._gather_modules(modules_dir)
self._modules_dir = modules_dir
self._packages_dir = packages_dir
self._all_arches = all_arches
@staticmethod
def _gather_modules(modules_dir):
@ -93,6 +94,7 @@ class KojiMock:
'name': module.name,
'id': module.build_id,
'tag_name': tag_name,
'arch': module.arch,
# Following fields are currently not
# used but returned by real koji
# left them here just for reference
@ -201,31 +203,12 @@ class KojiMock:
packages = []
# get all rpms in folder
rpms = search_rpms(self._packages_dir)
all_rpms = [package.path for package in rpms]
rpms = search_rpms(Path(self._packages_dir))
# get nvras for modular packages
nvras = set()
for module in self._modules.values():
path = os.path.join(
self._modules_dir,
module.arch,
module.nvr,
)
info = Modulemd.ModuleStream.read_string(open(path).read(), strict=True)
for package in info.get_rpm_artifacts():
data = parse_nvra(package)
nvras.add((data['name'], data['version'], data['release'], data['arch']))
# and remove modular packages from global list
for rpm in all_rpms[:]:
data = parse_nvra(os.path.basename(rpm[:-4]))
if (data['name'], data['version'], data['release'], data['arch']) in nvras:
all_rpms.remove(rpm)
for rpm in all_rpms:
info = parse_nvra(os.path.basename(rpm))
for rpm in rpms:
info = parse_nvra(rpm.path.stem)
if 'module' in info['release']:
continue
packages.append({
"build_id": RELEASE_BUILD_ID,
"name": info['name'],
@ -246,15 +229,19 @@ class KojiMock:
"""
Get list of builds for module and given module tag name.
"""
module = self._get_module_by_name(tag_name)
path = os.path.join(
self._modules_dir,
module.arch,
tag_name,
)
builds = []
packages = []
modules = self._get_modules_by_name(tag_name)
for module in modules:
if module is None:
raise ValueError('Module %s is not found' % tag_name)
path = os.path.join(
self._modules_dir,
module.arch,
tag_name,
)
builds = [
{
builds.append({
"build_id": module.build_id,
"package_name": module.name,
"nvr": module.nvr,
@ -280,35 +267,33 @@ class KojiMock:
# "volume_id": 0,
# "package_id": 104,
# "owner_id": 6,
}
]
if module is None:
raise ValueError('Module %s is not found' % tag_name)
})
packages = []
if os.path.exists(path):
info = Modulemd.ModuleStream.read_string(open(path).read(), strict=True)
for art in info.get_rpm_artifacts():
data = parse_nvra(art)
packages.append({
"build_id": module.build_id,
"name": data['name'],
"extra": None,
"arch": data['arch'],
"epoch": data['epoch'] or None,
"version": data['version'],
"metadata_only": False,
"release": data['release'],
"id": 262555,
"size": 0
})
else:
raise RuntimeError('Unable to find module %s' % path)
if os.path.exists(path):
info = Modulemd.ModuleStream.read_string(open(path).read(), strict=True)
for art in info.get_rpm_artifacts():
data = parse_nvra(art)
packages.append({
"build_id": module.build_id,
"name": data['name'],
"extra": None,
"arch": data['arch'],
"epoch": data['epoch'] or None,
"version": data['version'],
"metadata_only": False,
"release": data['release'],
"id": 262555,
"size": 0
})
else:
raise RuntimeError('Unable to find module %s' % path)
return builds, packages
def _get_module_by_name(self, tag_name):
for module in self._modules.values():
if module.nvr != tag_name:
continue
return module
return None
def _get_modules_by_name(self, tag_name):
modules = []
for arch in self._all_arches:
for module in self._modules.values():
if module.nvr != tag_name or module.arch != arch:
continue
modules.append(module)
return modules

View File

@ -14,17 +14,23 @@
# along with this program; if not, see <https://gnu.org/licenses/>.
import contextlib
import os
import re
import socket
import shutil
import time
import threading
import contextlib
import requests
import koji
from kobo.shortcuts import run, force_list
import six
from six.moves import configparser, shlex_quote
import six.moves.xmlrpc_client as xmlrpclib
from flufl.lock import Lock
from datetime import timedelta
from .kojimock import KojiMock
from .. import util
@ -786,11 +792,10 @@ class KojiWrapper(object):
if list_of_args is None and list_of_kwargs is None:
raise ValueError("One of list_of_args or list_of_kwargs must be set.")
if type(list_of_args) not in [type(None), list] or type(list_of_kwargs) not in [
type(None),
list,
]:
raise ValueError("list_of_args and list_of_kwargs must be list or None.")
if list_of_args is not None and not isinstance(list_of_args, list):
raise ValueError("list_of_args must be list or None.")
if list_of_kwargs is not None and not isinstance(list_of_kwargs, list):
raise ValueError("list_of_kwargs must be list or None.")
if list_of_kwargs is None:
list_of_kwargs = [{}] * len(list_of_args)
@ -804,9 +809,9 @@ class KojiWrapper(object):
koji_session.multicall = True
for args, kwargs in zip(list_of_args, list_of_kwargs):
if type(args) != list:
if not isinstance(args, list):
args = [args]
if type(kwargs) != dict:
if not isinstance(kwargs, dict):
raise ValueError("Every item in list_of_kwargs must be a dict")
koji_session_fnc(*args, **kwargs)
@ -814,7 +819,7 @@ class KojiWrapper(object):
if not responses:
return None
if type(responses) != list:
if not isinstance(responses, list):
raise ValueError(
"Fault element was returned for multicall of method %r: %r"
% (koji_session_fnc, responses)
@ -830,7 +835,7 @@ class KojiWrapper(object):
# a one-item array containing the result value,
# or a struct of the form found inside the standard <fault> element.
for response, args, kwargs in zip(responses, list_of_args, list_of_kwargs):
if type(response) == list:
if isinstance(response, list):
if not response:
raise ValueError(
"Empty list returned for multicall of method %r with args %r, %r" # noqa: E501
@ -868,10 +873,15 @@ class KojiWrapper(object):
class KojiMockWrapper(object):
lock = threading.Lock()
def __init__(self, profile):
self.profile = profile
def __init__(self, compose, all_arches):
self.all_arches = all_arches
self.compose = compose
try:
self.profile = self.compose.conf["koji_profile"]
except KeyError:
raise RuntimeError("Koji profile must be configured")
with self.lock:
self.koji_module = koji.get_profile_module(profile)
self.koji_module = koji.get_profile_module(self.profile)
session_opts = {}
for key in (
"timeout",
@ -891,7 +901,12 @@ class KojiMockWrapper(object):
session_opts[key] = value
self.koji_proxy = KojiMock(
packages_dir=self.koji_module.config.topdir,
modules_dir=os.path.join(self.koji_module.config.topdir, 'modules'))
modules_dir=os.path.join(
self.koji_module.config.topdir,
'modules',
),
all_arches=self.all_arches,
)
def get_buildroot_rpms(compose, task_id):
@ -925,3 +940,176 @@ def get_buildroot_rpms(compose, task_id):
continue
result.append(i)
return sorted(result)
class KojiDownloadProxy:
def __init__(self, topdir, topurl, cache_dir, logger):
if not topdir:
# This will only happen if there is either no koji_profile
# configured, or the profile doesn't have a topdir. In the first
# case there will be no koji interaction, and the second indicates
# broken koji configuration.
# We can pretend to have local access in both cases to avoid any
# external requests.
self.has_local_access = True
return
self.cache_dir = cache_dir
self.logger = logger
self.topdir = topdir
self.topurl = topurl
# If cache directory is configured, we want to use it (even if we
# actually have local access to the storage).
self.has_local_access = not bool(cache_dir)
# This is used for temporary downloaded files. The suffix is unique
# per-process. To prevent threads in the same process from colliding, a
# thread id is added later.
self.unique_suffix = "%s.%s" % (socket.gethostname(), os.getpid())
self.session = None
if not self.has_local_access:
self.session = requests.Session()
@property
def path_prefix(self):
dir = self.topdir if self.has_local_access else self.cache_dir
return dir.rstrip("/") + "/"
@classmethod
def from_config(klass, conf, logger):
topdir = None
topurl = None
cache_dir = None
if "koji_profile" in conf:
koji_module = koji.get_profile_module(conf["koji_profile"])
topdir = koji_module.config.topdir
topurl = koji_module.config.topurl
cache_dir = conf.get("koji_cache")
if cache_dir:
cache_dir = cache_dir.rstrip("/") + "/"
return klass(topdir, topurl, cache_dir, logger)
@util.retry(wait_on=requests.exceptions.RequestException)
def _download(self, url, dest):
"""Download file into given location
:param str url: URL of the file to download
:param str dest: file path to store the result in
:returns: path to the downloaded file (same as dest) or None if the URL
"""
with self.session.get(url, stream=True) as r:
if r.status_code == 404:
self.logger.warning("GET %s NOT FOUND", url)
return None
if r.status_code != 200:
self.logger.error("GET %s %s", url, r.status_code)
r.raise_for_status()
# The exception from here will be retried by the decorator.
file_size = int(r.headers.get("Content-Length", 0))
self.logger.info("GET %s OK %s", url, util.format_size(file_size))
with open(dest, "wb") as f:
shutil.copyfileobj(r.raw, f)
return dest
def _delete(self, path):
"""Try to delete file at given path and ignore errors."""
try:
os.remove(path)
except Exception:
self.logger.warning("Failed to delete %s", path)
def _atomic_download(self, url, dest, validator):
"""Atomically download a file
:param str url: URL of the file to download
:param str dest: file path to store the result in
:returns: path to the downloaded file (same as dest) or None if the URL
return 404.
"""
temp_file = "%s.%s.%s" % (dest, self.unique_suffix, threading.get_ident())
# First download to the temporary location.
try:
if self._download(url, temp_file) is None:
# The file was not found.
return None
except Exception:
# Download failed, let's make sure to clean up potentially partial
# temporary file.
self._delete(temp_file)
raise
# Check if the temporary file is correct (assuming we were provided a
# validator function).
try:
if validator:
validator(temp_file)
except Exception:
# Validation failed. Let's delete the problematic file and re-raise
# the exception.
self._delete(temp_file)
raise
# Atomically move the temporary file into final location
os.rename(temp_file, dest)
return dest
def _download_file(self, path, validator):
"""Ensure file on Koji volume in ``path`` is present in the local
cache.
:returns: path to the local file or None if file is not found
"""
url = path.replace(self.topdir, self.topurl)
destination_file = path.replace(self.topdir, self.cache_dir)
util.makedirs(os.path.dirname(destination_file))
lock = Lock(destination_file + ".lock")
# Hold the lock for this file for 5 minutes. If another compose needs
# the same file but it's not downloaded yet, the process will wait.
#
# If the download finishes in time, the downloaded file will be used
# here.
#
# If the download takes longer, this process will steal the lock and
# start its own download.
#
# That should not be a problem: the same file will be downloaded and
# then replaced atomically on the filesystem. If the original process
# managed to hardlink the first file already, that hardlink will be
# broken, but that will only result in the same file stored twice.
lock.lifetime = timedelta(minutes=5)
with lock:
# Check if the file already exists. If yes, return the path.
if os.path.exists(destination_file):
# Update mtime of the file. This covers the case of packages in the
# tag that are not included in the compose. Updating mtime will
# exempt them from cleanup for extra time.
os.utime(destination_file)
return destination_file
return self._atomic_download(url, destination_file, validator)
def get_file(self, path, validator=None):
"""
If path refers to an existing file in Koji, return a valid local path
to it. If no such file exists, return None.
:param validator: A callable that will be called with the path to the
downloaded file if and only if the file was actually downloaded.
Any exception raised from there will be abort the download and be
propagated.
"""
if self.has_local_access:
# We have koji volume mounted locally. No transformation needed for
# the path, just check it exists.
if os.path.exists(path):
return path
return None
else:
# We need to download the file.
return self._download_file(path, validator)

View File

@ -20,6 +20,7 @@ import os
import shutil
import glob
import six
import threading
from six.moves import shlex_quote
from six.moves.urllib.request import urlretrieve
from fnmatch import fnmatch
@ -29,12 +30,15 @@ from kobo.shortcuts import run, force_list
from pungi.util import explode_rpm_package, makedirs, copy_all, temp_dir, retry
from .kojiwrapper import KojiWrapper
lock = threading.Lock()
class ScmBase(kobo.log.LoggingBase):
def __init__(self, logger=None, command=None, compose=None):
def __init__(self, logger=None, command=None, compose=None, options=None):
kobo.log.LoggingBase.__init__(self, logger=logger)
self.command = command
self.compose = compose
self.options = options or {}
@retry(interval=60, timeout=300, wait_on=RuntimeError)
def retry_run(self, cmd, **kwargs):
@ -156,22 +160,31 @@ class GitWrapper(ScmBase):
if "://" not in repo:
repo = "file://%s" % repo
git_cmd = ["git"]
if "credential_helper" in self.options:
git_cmd.extend(["-c", "credential.useHttpPath=true"])
git_cmd.extend(
["-c", "credential.helper=%s" % self.options["credential_helper"]]
)
run(["git", "init"], workdir=destdir)
try:
run(["git", "fetch", "--depth=1", repo, branch], workdir=destdir)
run(git_cmd + ["fetch", "--depth=1", repo, branch], workdir=destdir)
run(["git", "checkout", "FETCH_HEAD"], workdir=destdir)
except RuntimeError as e:
# Fetch failed, to do a full clone we add a remote to our empty
# repo, get its content and check out the reference we want.
self.log_debug(
"Trying to do a full clone because shallow clone failed: %s %s"
% (e, e.output)
% (e, getattr(e, "output", ""))
)
try:
# Re-run git init in case of previous failure breaking .git dir
run(["git", "init"], workdir=destdir)
run(["git", "remote", "add", "origin", repo], workdir=destdir)
self.retry_run(["git", "remote", "update", "origin"], workdir=destdir)
self.retry_run(
git_cmd + ["remote", "update", "origin"], workdir=destdir
)
run(["git", "checkout", branch], workdir=destdir)
except RuntimeError:
if self.compose:
@ -185,19 +198,38 @@ class GitWrapper(ScmBase):
copy_all(destdir, debugdir)
raise
self.run_process_command(destdir)
def get_temp_repo_path(self, scm_root, scm_branch):
scm_repo = scm_root.split("/")[-1]
process_id = os.getpid()
tmp_dir = (
"/tmp/pungi-temp-git-repos-"
+ str(process_id)
+ "/"
+ scm_repo
+ "-"
+ scm_branch
)
return tmp_dir
def setup_repo(self, scm_root, scm_branch):
tmp_dir = self.get_temp_repo_path(scm_root, scm_branch)
if not os.path.isdir(tmp_dir):
makedirs(tmp_dir)
self._clone(scm_root, scm_branch, tmp_dir)
self.run_process_command(tmp_dir)
return tmp_dir
def export_dir(self, scm_root, scm_dir, target_dir, scm_branch=None):
scm_dir = scm_dir.lstrip("/")
scm_branch = scm_branch or "master"
with temp_dir() as tmp_dir:
self.log_debug(
"Exporting directory %s from git %s (branch %s)..."
% (scm_dir, scm_root, scm_branch)
)
self.log_debug(
"Exporting directory %s from git %s (branch %s)..."
% (scm_dir, scm_root, scm_branch)
)
self._clone(scm_root, scm_branch, tmp_dir)
with lock:
tmp_dir = self.setup_repo(scm_root, scm_branch)
copy_all(os.path.join(tmp_dir, scm_dir), target_dir)
@ -205,15 +237,15 @@ class GitWrapper(ScmBase):
scm_file = scm_file.lstrip("/")
scm_branch = scm_branch or "master"
with temp_dir() as tmp_dir:
target_path = os.path.join(target_dir, os.path.basename(scm_file))
target_path = os.path.join(target_dir, os.path.basename(scm_file))
self.log_debug(
"Exporting file %s from git %s (branch %s)..."
% (scm_file, scm_root, scm_branch)
)
self.log_debug(
"Exporting file %s from git %s (branch %s)..."
% (scm_file, scm_root, scm_branch)
)
self._clone(scm_root, scm_branch, tmp_dir)
with lock:
tmp_dir = self.setup_repo(scm_root, scm_branch)
makedirs(target_dir)
shutil.copy2(os.path.join(tmp_dir, scm_file), target_path)
@ -361,15 +393,19 @@ def get_file_from_scm(scm_dict, target_path, compose=None):
scm_file = os.path.abspath(scm_dict)
scm_branch = None
command = None
options = {}
else:
scm_type = scm_dict["scm"]
scm_repo = scm_dict["repo"]
scm_file = scm_dict["file"]
scm_branch = scm_dict.get("branch", None)
command = scm_dict.get("command")
options = scm_dict.get("options", {})
logger = compose._logger if compose else None
scm = _get_wrapper(scm_type, logger=logger, command=command, compose=compose)
scm = _get_wrapper(
scm_type, logger=logger, command=command, compose=compose, options=options
)
files_copied = []
for i in force_list(scm_file):
@ -450,15 +486,19 @@ def get_dir_from_scm(scm_dict, target_path, compose=None):
scm_dir = os.path.abspath(scm_dict)
scm_branch = None
command = None
options = {}
else:
scm_type = scm_dict["scm"]
scm_repo = scm_dict.get("repo", None)
scm_dir = scm_dict["dir"]
scm_branch = scm_dict.get("branch", None)
command = scm_dict.get("command")
options = scm_dict.get("options", {})
logger = compose._logger if compose else None
scm = _get_wrapper(scm_type, logger=logger, command=command, compose=compose)
scm = _get_wrapper(
scm_type, logger=logger, command=command, compose=compose, options=options
)
with temp_dir(prefix="scm_checkout_") as tmp_dir:
scm.export_dir(scm_repo, scm_dir, scm_branch=scm_branch, target_dir=tmp_dir)

View File

@ -276,7 +276,6 @@ class Variant(object):
modules=None,
modular_koji_tags=None,
):
environments = environments or []
buildinstallpackages = buildinstallpackages or []

View File

@ -1,705 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import print_function
import argparse
import atexit
import errno
import json
import logging
import os
import re
import shutil
import subprocess
import sys
import tempfile
import time
import threading
from collections import namedtuple
import kobo.conf
import kobo.log
import productmd
from kobo import shortcuts
from six.moves import configparser, shlex_quote
import pungi.util
from pungi.compose import get_compose_dir
from pungi.linker import linker_pool
from pungi.phases.pkgset.sources.source_koji import get_koji_event_raw
from pungi.util import find_old_compose, parse_koji_event, temp_dir
from pungi.wrappers.kojiwrapper import KojiWrapper
Config = namedtuple(
"Config",
[
# Path to directory with the compose
"target",
"compose_type",
"label",
# Path to the selected old compose that will be reused
"old_compose",
# Path to directory with config file copies
"config_dir",
# Which koji event to use (if any)
"event",
# Additional arguments to pungi-koji executable
"extra_args",
],
)
log = logging.getLogger(__name__)
class Status(object):
# Ready to start
READY = "READY"
# Waiting for dependencies to finish.
WAITING = "WAITING"
# Part is currently running
STARTED = "STARTED"
# A dependency failed, this one will never start.
BLOCKED = "BLOCKED"
class ComposePart(object):
def __init__(self, name, config, just_phase=[], skip_phase=[], dependencies=[]):
self.name = name
self.config = config
self.status = Status.WAITING if dependencies else Status.READY
self.just_phase = just_phase
self.skip_phase = skip_phase
self.blocked_on = set(dependencies)
self.depends_on = set(dependencies)
self.path = None
self.log_file = None
self.failable = False
def __str__(self):
return self.name
def __repr__(self):
return (
"ComposePart({0.name!r},"
" {0.config!r},"
" {0.status!r},"
" just_phase={0.just_phase!r},"
" skip_phase={0.skip_phase!r},"
" dependencies={0.depends_on!r})"
).format(self)
def refresh_status(self):
"""Refresh status of this part with the result of the compose. This
should only be called once the compose finished.
"""
try:
with open(os.path.join(self.path, "STATUS")) as fh:
self.status = fh.read().strip()
except IOError as exc:
log.error("Failed to update status of %s: %s", self.name, exc)
log.error("Assuming %s is DOOMED", self.name)
self.status = "DOOMED"
def is_finished(self):
return "FINISHED" in self.status
def unblock_on(self, finished_part):
"""Update set of blockers for this part. If it's empty, mark us as ready."""
self.blocked_on.discard(finished_part)
if self.status == Status.WAITING and not self.blocked_on:
log.debug("%s is ready to start", self)
self.status = Status.READY
def setup_start(self, global_config, parts):
substitutions = dict(
("part-%s" % name, p.path) for name, p in parts.items() if p.is_finished()
)
substitutions["configdir"] = global_config.config_dir
config = pungi.util.load_config(self.config)
for f in config.opened_files:
# apply substitutions
fill_in_config_file(f, substitutions)
self.status = Status.STARTED
self.path = get_compose_dir(
os.path.join(global_config.target, "parts"),
config,
compose_type=global_config.compose_type,
compose_label=global_config.label,
)
self.log_file = os.path.join(global_config.target, "logs", "%s.log" % self.name)
log.info("Starting %s in %s", self.name, self.path)
def get_cmd(self, global_config):
cmd = ["pungi-koji", "--config", self.config, "--compose-dir", self.path]
cmd.append("--%s" % global_config.compose_type)
if global_config.label:
cmd.extend(["--label", global_config.label])
for phase in self.just_phase:
cmd.extend(["--just-phase", phase])
for phase in self.skip_phase:
cmd.extend(["--skip-phase", phase])
if global_config.old_compose:
cmd.extend(
["--old-compose", os.path.join(global_config.old_compose, "parts")]
)
if global_config.event:
cmd.extend(["--koji-event", str(global_config.event)])
if global_config.extra_args:
cmd.extend(global_config.extra_args)
cmd.extend(["--no-latest-link"])
return cmd
@classmethod
def from_config(cls, config, section, config_dir):
part = cls(
name=section,
config=os.path.join(config_dir, config.get(section, "config")),
just_phase=_safe_get_list(config, section, "just_phase", []),
skip_phase=_safe_get_list(config, section, "skip_phase", []),
dependencies=_safe_get_list(config, section, "depends_on", []),
)
if config.has_option(section, "failable"):
part.failable = config.getboolean(section, "failable")
return part
def _safe_get_list(config, section, option, default=None):
"""Get a value from config parser. The result is split into a list on
commas or spaces, and `default` is returned if the key does not exist.
"""
if config.has_option(section, option):
value = config.get(section, option)
return [x.strip() for x in re.split(r"[, ]+", value) if x]
return default
def fill_in_config_file(fp, substs):
"""Templating function. It works with Jinja2 style placeholders such as
{{foo}}. Whitespace around the key name is fine. The file is modified in place.
:param fp string: path to the file to process
:param substs dict: a mapping for values to put into the file
"""
def repl(match):
try:
return substs[match.group(1)]
except KeyError as exc:
raise RuntimeError(
"Unknown placeholder %s in %s" % (exc, os.path.basename(fp))
)
with open(fp, "r") as f:
contents = re.sub(r"{{ *([a-zA-Z-_]+) *}}", repl, f.read())
with open(fp, "w") as f:
f.write(contents)
def start_part(global_config, parts, part):
part.setup_start(global_config, parts)
fh = open(part.log_file, "w")
cmd = part.get_cmd(global_config)
log.debug("Running command %r", " ".join(shlex_quote(x) for x in cmd))
return subprocess.Popen(cmd, stdout=fh, stderr=subprocess.STDOUT)
def handle_finished(global_config, linker, parts, proc, finished_part):
finished_part.refresh_status()
log.info("%s finished with status %s", finished_part, finished_part.status)
if proc.returncode == 0:
# Success, unblock other parts...
for part in parts.values():
part.unblock_on(finished_part.name)
# ...and link the results into final destination.
copy_part(global_config, linker, finished_part)
update_metadata(global_config, finished_part)
else:
# Failure, other stuff may be blocked.
log.info("See details in %s", finished_part.log_file)
block_on(parts, finished_part.name)
def copy_part(global_config, linker, part):
c = productmd.Compose(part.path)
for variant in c.info.variants:
data_path = os.path.join(part.path, "compose", variant)
link = os.path.join(global_config.target, "compose", variant)
log.info("Hardlinking content %s -> %s", data_path, link)
hardlink_dir(linker, data_path, link)
def hardlink_dir(linker, srcdir, dstdir):
for root, dirs, files in os.walk(srcdir):
root = os.path.relpath(root, srcdir)
for f in files:
src = os.path.normpath(os.path.join(srcdir, root, f))
dst = os.path.normpath(os.path.join(dstdir, root, f))
linker.queue_put((src, dst))
def update_metadata(global_config, part):
part_metadata_dir = os.path.join(part.path, "compose", "metadata")
final_metadata_dir = os.path.join(global_config.target, "compose", "metadata")
for f in os.listdir(part_metadata_dir):
# Load the metadata
with open(os.path.join(part_metadata_dir, f)) as fh:
part_metadata = json.load(fh)
final_metadata = os.path.join(final_metadata_dir, f)
if os.path.exists(final_metadata):
# We already have this file, will need to merge.
merge_metadata(final_metadata, part_metadata)
else:
# A new file, just copy it.
copy_metadata(global_config, final_metadata, part_metadata)
def copy_metadata(global_config, final_metadata, source):
"""Copy file to final location, but update compose information."""
with open(
os.path.join(global_config.target, "compose/metadata/composeinfo.json")
) as f:
composeinfo = json.load(f)
try:
source["payload"]["compose"].update(composeinfo["payload"]["compose"])
except KeyError:
# No [payload][compose], probably OSBS metadata
pass
with open(final_metadata, "w") as f:
json.dump(source, f, indent=2, sort_keys=True)
def merge_metadata(final_metadata, source):
with open(final_metadata) as f:
metadata = json.load(f)
try:
key = {
"productmd.composeinfo": "variants",
"productmd.modules": "modules",
"productmd.images": "images",
"productmd.rpms": "rpms",
}[source["header"]["type"]]
# TODO what if multiple parts create images for the same variant
metadata["payload"][key].update(source["payload"][key])
except KeyError:
# OSBS metadata, merge whole file
metadata.update(source)
with open(final_metadata, "w") as f:
json.dump(metadata, f, indent=2, sort_keys=True)
def block_on(parts, name):
"""Part ``name`` failed, mark everything depending on it as blocked."""
for part in parts.values():
if name in part.blocked_on:
log.warning("%s is blocked now and will not run", part)
part.status = Status.BLOCKED
block_on(parts, part.name)
def check_finished_processes(processes):
"""Walk through all active processes and check if something finished."""
for proc in processes.keys():
proc.poll()
if proc.returncode is not None:
yield proc, processes[proc]
def run_all(global_config, parts):
# Mapping subprocess.Popen -> ComposePart
processes = dict()
remaining = set(p.name for p in parts.values() if not p.is_finished())
with linker_pool("hardlink") as linker:
while remaining or processes:
update_status(global_config, parts)
for proc, part in check_finished_processes(processes):
del processes[proc]
handle_finished(global_config, linker, parts, proc, part)
# Start new available processes.
for name in list(remaining):
part = parts[name]
# Start all ready parts
if part.status == Status.READY:
remaining.remove(name)
processes[start_part(global_config, parts, part)] = part
# Remove blocked parts from todo list
elif part.status == Status.BLOCKED:
remaining.remove(part.name)
# Wait for any child process to finish if there is any.
if processes:
pid, reason = os.wait()
for proc in processes.keys():
# Set the return code for process that we caught by os.wait().
# Calling poll() on it would not set the return code properly
# since the value was already consumed by os.wait().
if proc.pid == pid:
proc.returncode = (reason >> 8) & 0xFF
log.info("Waiting for linking to finish...")
return update_status(global_config, parts)
def get_target_dir(config, compose_info, label, reldir=""):
"""Find directory where this compose will be.
@param reldir: if target path in config is relative, it will be resolved
against this directory
"""
dir = os.path.realpath(os.path.join(reldir, config.get("general", "target")))
target_dir = get_compose_dir(
dir,
compose_info,
compose_type=config.get("general", "compose_type"),
compose_label=label,
)
return target_dir
def setup_logging(debug=False):
FORMAT = "%(asctime)s: %(levelname)s: %(message)s"
level = logging.DEBUG if debug else logging.INFO
kobo.log.add_stderr_logger(log, log_level=level, format=FORMAT)
log.setLevel(level)
def compute_status(statuses):
if any(map(lambda x: x[0] in ("STARTED", "WAITING"), statuses)):
# If there is anything still running or waiting to start, the whole is
# still running.
return "STARTED"
elif any(map(lambda x: x[0] in ("DOOMED", "BLOCKED") and not x[1], statuses)):
# If any required part is doomed or blocked, the whole is doomed
return "DOOMED"
elif all(map(lambda x: x[0] == "FINISHED", statuses)):
# If all parts are complete, the whole is complete
return "FINISHED"
else:
return "FINISHED_INCOMPLETE"
def update_status(global_config, parts):
log.debug("Updating status metadata")
metadata = {}
statuses = set()
for part in parts.values():
metadata[part.name] = {"status": part.status, "path": part.path}
statuses.add((part.status, part.failable))
metadata_path = os.path.join(
global_config.target, "compose", "metadata", "parts.json"
)
with open(metadata_path, "w") as fh:
json.dump(metadata, fh, indent=2, sort_keys=True, separators=(",", ": "))
status = compute_status(statuses)
log.info("Overall status is %s", status)
with open(os.path.join(global_config.target, "STATUS"), "w") as fh:
fh.write(status)
return status != "DOOMED"
def prepare_compose_dir(config, args, main_config_file, compose_info):
if not hasattr(args, "compose_path"):
# Creating a brand new compose
target_dir = get_target_dir(
config, compose_info, args.label, reldir=os.path.dirname(main_config_file)
)
for dir in ("logs", "parts", "compose/metadata", "work/global"):
try:
os.makedirs(os.path.join(target_dir, dir))
except OSError as exc:
if exc.errno != errno.EEXIST:
raise
with open(os.path.join(target_dir, "STATUS"), "w") as fh:
fh.write("STARTED")
# Copy initial composeinfo for new compose
shutil.copy(
os.path.join(target_dir, "work/global/composeinfo-base.json"),
os.path.join(target_dir, "compose/metadata/composeinfo.json"),
)
else:
# Restarting a particular compose
target_dir = args.compose_path
return target_dir
def load_parts_metadata(global_config):
parts_metadata = os.path.join(global_config.target, "compose/metadata/parts.json")
with open(parts_metadata) as f:
return json.load(f)
def setup_for_restart(global_config, parts, to_restart):
has_stuff_to_do = False
metadata = load_parts_metadata(global_config)
for key in metadata:
# Update state to match what is on disk
log.debug(
"Reusing %s (%s) from %s",
key,
metadata[key]["status"],
metadata[key]["path"],
)
parts[key].status = metadata[key]["status"]
parts[key].path = metadata[key]["path"]
for key in to_restart:
# Set restarted parts to run again
parts[key].status = Status.WAITING
parts[key].path = None
for key in to_restart:
# Remove blockers that are already finished
for blocker in list(parts[key].blocked_on):
if parts[blocker].is_finished():
parts[key].blocked_on.discard(blocker)
if not parts[key].blocked_on:
log.debug("Part %s in not blocked", key)
# Nothing blocks it; let's go
parts[key].status = Status.READY
has_stuff_to_do = True
if not has_stuff_to_do:
raise RuntimeError("All restarted parts are blocked. Nothing to do.")
def run_kinit(config):
if not config.getboolean("general", "kerberos"):
return
keytab = config.get("general", "kerberos_keytab")
principal = config.get("general", "kerberos_principal")
fd, fname = tempfile.mkstemp(prefix="krb5cc_pungi-orchestrate_")
os.close(fd)
os.environ["KRB5CCNAME"] = fname
shortcuts.run(["kinit", "-k", "-t", keytab, principal])
log.debug("Created a kerberos ticket for %s", principal)
atexit.register(os.remove, fname)
def get_compose_data(compose_path):
try:
compose = productmd.compose.Compose(compose_path)
data = {
"compose_id": compose.info.compose.id,
"compose_date": compose.info.compose.date,
"compose_type": compose.info.compose.type,
"compose_respin": str(compose.info.compose.respin),
"compose_label": compose.info.compose.label,
"release_id": compose.info.release_id,
"release_name": compose.info.release.name,
"release_short": compose.info.release.short,
"release_version": compose.info.release.version,
"release_type": compose.info.release.type,
"release_is_layered": compose.info.release.is_layered,
}
if compose.info.release.is_layered:
data.update(
{
"base_product_name": compose.info.base_product.name,
"base_product_short": compose.info.base_product.short,
"base_product_version": compose.info.base_product.version,
"base_product_type": compose.info.base_product.type,
}
)
return data
except Exception:
return {}
def get_script_env(compose_path):
env = os.environ.copy()
env["COMPOSE_PATH"] = compose_path
for key, value in get_compose_data(compose_path).items():
if isinstance(value, bool):
env[key.upper()] = "YES" if value else ""
else:
env[key.upper()] = str(value) if value else ""
return env
def run_scripts(prefix, compose_dir, scripts):
env = get_script_env(compose_dir)
for idx, script in enumerate(scripts.strip().splitlines()):
command = script.strip()
logfile = os.path.join(compose_dir, "logs", "%s%s.log" % (prefix, idx))
log.debug("Running command: %r", command)
log.debug("See output in %s", logfile)
shortcuts.run(command, env=env, logfile=logfile)
def try_translate_path(parts, path):
translation = []
for part in parts.values():
conf = pungi.util.load_config(part.config)
translation.extend(conf.get("translate_paths", []))
return pungi.util.translate_path_raw(translation, path)
def send_notification(compose_dir, command, parts):
if not command:
return
from pungi.notifier import PungiNotifier
data = get_compose_data(compose_dir)
data["location"] = try_translate_path(parts, compose_dir)
notifier = PungiNotifier([command])
with open(os.path.join(compose_dir, "STATUS")) as f:
status = f.read().strip()
notifier.send("status-change", workdir=compose_dir, status=status, **data)
def setup_progress_monitor(global_config, parts):
"""Update configuration so that each part send notifications about its
progress to the orchestrator.
There is a file to which the notification is written. The orchestrator is
reading it and mapping the entries to particular parts. The path to this
file is stored in an environment variable.
"""
tmp_file = tempfile.NamedTemporaryFile(prefix="pungi-progress-monitor_")
os.environ["_PUNGI_ORCHESTRATOR_PROGRESS_MONITOR"] = tmp_file.name
atexit.register(os.remove, tmp_file.name)
global_config.extra_args.append(
"--notification-script=pungi-notification-report-progress"
)
def reader():
while True:
line = tmp_file.readline()
if not line:
time.sleep(0.1)
continue
path, msg = line.split(":", 1)
for part in parts:
if parts[part].path == os.path.dirname(path):
log.debug("%s: %s", part, msg.strip())
break
monitor = threading.Thread(target=reader)
monitor.daemon = True
monitor.start()
def run(work_dir, main_config_file, args):
config_dir = os.path.join(work_dir, "config")
shutil.copytree(os.path.dirname(main_config_file), config_dir)
# Read main config
parser = configparser.RawConfigParser(
defaults={
"kerberos": "false",
"pre_compose_script": "",
"post_compose_script": "",
"notification_script": "",
}
)
parser.read(main_config_file)
# Create kerberos ticket
run_kinit(parser)
compose_info = dict(parser.items("general"))
compose_type = parser.get("general", "compose_type")
target_dir = prepare_compose_dir(parser, args, main_config_file, compose_info)
kobo.log.add_file_logger(log, os.path.join(target_dir, "logs", "orchestrator.log"))
log.info("Composing %s", target_dir)
run_scripts("pre_compose_", target_dir, parser.get("general", "pre_compose_script"))
old_compose = find_old_compose(
os.path.dirname(target_dir),
compose_info["release_short"],
compose_info["release_version"],
"",
)
if old_compose:
log.info("Reusing old compose %s", old_compose)
global_config = Config(
target=target_dir,
compose_type=compose_type,
label=args.label,
old_compose=old_compose,
config_dir=os.path.dirname(main_config_file),
event=args.koji_event,
extra_args=_safe_get_list(parser, "general", "extra_args"),
)
if not global_config.event and parser.has_option("general", "koji_profile"):
koji_wrapper = KojiWrapper(parser.get("general", "koji_profile"))
event_file = os.path.join(global_config.target, "work/global/koji-event")
result = get_koji_event_raw(koji_wrapper, None, event_file)
global_config = global_config._replace(event=result["id"])
parts = {}
for section in parser.sections():
if section == "general":
continue
parts[section] = ComposePart.from_config(parser, section, config_dir)
if hasattr(args, "part"):
setup_for_restart(global_config, parts, args.part)
setup_progress_monitor(global_config, parts)
send_notification(target_dir, parser.get("general", "notification_script"), parts)
retcode = run_all(global_config, parts)
if retcode:
# Only run the script if we are not doomed.
run_scripts(
"post_compose_", target_dir, parser.get("general", "post_compose_script")
)
send_notification(target_dir, parser.get("general", "notification_script"), parts)
return retcode
def parse_args(argv):
parser = argparse.ArgumentParser()
parser.add_argument("--debug", action="store_true")
parser.add_argument("--koji-event", metavar="ID", type=parse_koji_event)
subparsers = parser.add_subparsers()
start = subparsers.add_parser("start")
start.add_argument("config", metavar="CONFIG")
start.add_argument("--label")
restart = subparsers.add_parser("restart")
restart.add_argument("config", metavar="CONFIG")
restart.add_argument("compose_path", metavar="COMPOSE_PATH")
restart.add_argument(
"part", metavar="PART", nargs="*", help="which parts to restart"
)
restart.add_argument("--label")
return parser.parse_args(argv)
def main(argv=None):
args = parse_args(argv)
setup_logging(args.debug)
main_config_file = os.path.abspath(args.config)
with temp_dir() as work_dir:
try:
if not run(work_dir, main_config_file, args):
sys.exit(1)
except Exception:
log.exception("Unhandled exception!")
sys.exit(1)

View File

@ -1,7 +1,8 @@
# Some packages must be installed via dnf/yum first, see doc/contributing.rst
dict.sorted
dogpile.cache
fedmsg
flufl.lock ; python_version >= '3.0'
flufl.lock < 3.0 ; python_version <= '2.7'
funcsigs
jsonschema
kobo

2
setup.cfg Normal file
View File

@ -0,0 +1,2 @@
[sdist]
formats=bztar

View File

@ -5,14 +5,9 @@
import os
import glob
import distutils.command.sdist
from setuptools import setup
# override default tarball format with bzip2
distutils.command.sdist.sdist.default_format = {"posix": "bztar"}
# recursively scan for python modules to be included
package_root_dirs = ["pungi", "pungi_utils"]
packages = set()
@ -25,7 +20,7 @@ packages = sorted(packages)
setup(
name="pungi",
version="4.3.6",
version="4.5.0",
description="Distribution compose tool",
url="https://pagure.io/pungi",
author="Dennis Gilmore",
@ -41,12 +36,12 @@ setup(
"pungi-patch-iso = pungi.scripts.patch_iso:cli_main",
"pungi-make-ostree = pungi.ostree:main",
"pungi-notification-report-progress = pungi.scripts.report_progress:main",
"pungi-orchestrate = pungi_utils.orchestrator:main",
"pungi-wait-for-signed-ostree-handler = pungi.scripts.wait_for_signed_ostree_handler:main", # noqa: E501
"pungi-koji = pungi.scripts.pungi_koji:cli_main",
"pungi-gather = pungi.scripts.pungi_gather:cli_main",
"pungi-config-dump = pungi.scripts.config_dump:cli_main",
"pungi-config-validate = pungi.scripts.config_validate:cli_main",
"pungi-cache-cleanup = pungi.scripts.cache_cleanup:main",
"pungi-gather-modules = pungi.scripts.gather_modules:cli_main",
"pungi-gather-rpms = pungi.scripts.gather_rpms:cli_main",
"pungi-generate-packages-json = pungi.scripts.create_packages_json:cli_main", # noqa: E501
@ -55,6 +50,7 @@ setup(
},
scripts=["contrib/yum-dnf-compare/pungi-compare-depsolving"],
data_files=[
("/usr/lib/tmpfiles.d", glob.glob("contrib/tmpfiles.d/*.conf")),
("/usr/share/pungi", glob.glob("share/*.xsl")),
("/usr/share/pungi", glob.glob("share/*.ks")),
("/usr/share/pungi", glob.glob("share/*.dtd")),

View File

@ -108,6 +108,7 @@
<groupid>core</groupid>
</grouplist>
<optionlist>
<groupid arch="x86_64">standard</groupid>
</optionlist>
</environment>

View File

@ -21,6 +21,15 @@ from pungi import paths, checks
from pungi.module_util import Modulemd
GIT_WITH_CREDS = [
"git",
"-c",
"credential.useHttpPath=true",
"-c",
"credential.helper=!ch",
]
class BaseTestCase(unittest.TestCase):
def assertFilesEqual(self, fn1, fn2):
with open(fn1, "rb") as f1:
@ -158,6 +167,20 @@ class IterableMock(mock.Mock):
return iter([])
class FSKojiDownloader(object):
"""Mock for KojiDownloadProxy that checks provided path."""
def get_file(self, path, validator=None):
return path if os.path.isfile(path) else None
class DummyKojiDownloader(object):
"""Mock for KojiDownloadProxy that always finds the file in original location."""
def get_file(self, path, validator=None):
return path
class DummyCompose(object):
def __init__(self, topdir, config):
self.supported = True
@ -232,6 +255,8 @@ class DummyCompose(object):
self.cache_region = None
self.containers_metadata = {}
self.load_old_compose_config = mock.Mock(return_value=None)
self.koji_downloader = DummyKojiDownloader()
self.koji_downloader.path_prefix = "/prefix"
def setup_optional(self):
self.all_variants["Server-optional"] = MockVariant(
@ -272,7 +297,7 @@ class DummyCompose(object):
return tempfile.mkdtemp(suffix=suffix, prefix=prefix, dir=self.topdir)
def touch(path, content=None):
def touch(path, content=None, mode=None):
"""Helper utility that creates an dummy file in given location. Directories
will be created."""
content = content or (path + "\n")
@ -284,6 +309,8 @@ def touch(path, content=None):
content = content.encode()
with open(path, "wb") as f:
f.write(content)
if mode:
os.chmod(path, mode)
return path

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
import mock
from unittest import mock
import unittest
from pungi.arch import (

View File

@ -1,4 +1,4 @@
import mock
from unittest import mock
try:
import unittest2 as unittest

View File

@ -1209,6 +1209,7 @@ class BuildinstallThreadTestCase(PungiTestCase):
"buildinstall_method": "lorax",
"runroot_tag": "rrt",
"koji_profile": "koji",
"koji_cache": "/tmp",
"runroot_weights": {"buildinstall": 123},
},
)
@ -1308,6 +1309,7 @@ class BuildinstallThreadTestCase(PungiTestCase):
"lorax_use_koji_plugin": True,
"runroot_tag": "rrt",
"koji_profile": "koji",
"koji_cache": "/tmp",
"runroot_weights": {"buildinstall": 123},
},
)
@ -1412,6 +1414,7 @@ class BuildinstallThreadTestCase(PungiTestCase):
"buildinstall_method": "buildinstall",
"runroot_tag": "rrt",
"koji_profile": "koji",
"koji_cache": "/tmp",
},
)
@ -1500,6 +1503,7 @@ class BuildinstallThreadTestCase(PungiTestCase):
"buildinstall_method": "buildinstall",
"runroot_tag": "rrt",
"koji_profile": "koji",
"koji_cache": "/tmp",
"failable_deliverables": [("^.+$", {"*": ["buildinstall"]})],
},
)
@ -1542,6 +1546,7 @@ class BuildinstallThreadTestCase(PungiTestCase):
"buildinstall_method": "lorax",
"runroot_tag": "rrt",
"koji_profile": "koji",
"koji_cache": "/tmp",
"failable_deliverables": [("^.+$", {"*": ["buildinstall"]})],
},
)
@ -1591,6 +1596,7 @@ class BuildinstallThreadTestCase(PungiTestCase):
"buildinstall_method": "lorax",
"runroot_tag": "rrt",
"koji_profile": "koji",
"koji_cache": "/tmp",
"failable_deliverables": [("^.+$", {"*": ["buildinstall"]})],
},
)
@ -1663,6 +1669,7 @@ class BuildinstallThreadTestCase(PungiTestCase):
"buildinstall_method": "lorax",
"runroot_tag": "rrt",
"koji_profile": "koji",
"koji_cache": "/tmp",
"failable_deliverables": [("^.+$", {"*": ["buildinstall"]})],
},
)
@ -1701,6 +1708,7 @@ class BuildinstallThreadTestCase(PungiTestCase):
"buildinstall_method": "lorax",
"runroot_tag": "rrt",
"koji_profile": "koji",
"koji_cache": "/tmp",
"runroot_weights": {"buildinstall": 123},
"buildinstall_topdir": "/buildinstall_topdir",
},
@ -1810,6 +1818,7 @@ class BuildinstallThreadTestCase(PungiTestCase):
"buildinstall_method": "lorax",
"runroot_tag": "rrt",
"koji_profile": "koji",
"koji_cache": "/tmp",
},
)

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
import mock
from unittest import mock
try:
import unittest2 as unittest

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
import logging
import mock
from unittest import mock
try:
import unittest2 as unittest
@ -628,6 +628,7 @@ class ComposeTestCase(unittest.TestCase):
ci_copy = dict(self.ci_json)
ci_copy["header"]["version"] = "1.2"
mocked_response = mock.MagicMock()
mocked_response.status_code = 200
mocked_response.text = json.dumps(self.ci_json)
mocked_requests.post.return_value = mocked_response
@ -655,6 +656,7 @@ class ComposeTestCase(unittest.TestCase):
mocked_requests.post.assert_called_once_with(
"https://cts.localhost.tld/api/1/composes/",
auth=mock.ANY,
data=None,
json=expected_json,
)
@ -793,12 +795,16 @@ class TracebackTest(unittest.TestCase):
shutil.rmtree(self.tmp_dir)
self.patcher.stop()
def assertTraceback(self, filename):
def assertTraceback(self, filename, show_locals=True):
self.assertTrue(
os.path.isfile("%s/logs/global/%s.global.log" % (self.tmp_dir, filename))
)
self.assertEqual(
self.Traceback.mock_calls, [mock.call(), mock.call().get_traceback()]
self.Traceback.mock_calls,
[
mock.call(show_locals=show_locals),
mock.call(show_locals=show_locals).get_traceback(),
],
)
def test_traceback_default(self):
@ -811,6 +817,7 @@ class TracebackTest(unittest.TestCase):
class RetryRequestTest(unittest.TestCase):
@mock.patch("time.sleep", new=lambda x: x)
@mock.patch("pungi.compose.requests")
def test_retry_timeout(self, mocked_requests):
mocked_requests.post.side_effect = [
@ -822,8 +829,22 @@ class RetryRequestTest(unittest.TestCase):
self.assertEqual(
mocked_requests.mock_calls,
[
mock.call.post(url, json=None, auth=None),
mock.call.post(url, json=None, auth=None),
mock.call.post(url, data=None, json=None, auth=None),
mock.call.post(url, data=None, json=None, auth=None),
],
)
self.assertEqual(rv.status_code, 200)
@mock.patch("pungi.compose.requests")
def test_no_retry_on_client_error(self, mocked_requests):
mocked_requests.post.side_effect = [
mock.Mock(status_code=400, json=lambda: {"message": "You made a mistake"}),
]
url = "http://locahost/api/1/composes/"
with self.assertRaises(RuntimeError):
retry_request("post", url)
self.assertEqual(
mocked_requests.mock_calls,
[mock.call.post(url, data=None, json=None, auth=None)],
)

View File

@ -7,7 +7,7 @@ except ImportError:
import unittest
import six
import mock
from unittest import mock
from pungi import checks
from tests.helpers import load_config, PKGSET_REPOS
@ -440,7 +440,7 @@ class LiveMediaConfigTestCase(ConfigTestCase):
live_media_version="Rawhide",
)
resolve_git_url.side_effect = lambda x: x.replace("HEAD", "CAFE")
resolve_git_url.side_effect = lambda x, _helper: x.replace("HEAD", "CAFE")
self.assertValidation(cfg)
self.assertEqual(cfg["live_media_ksurl"], "git://example.com/repo.git#CAFE")

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
import mock
from unittest import mock
import os
import six

View File

@ -5,7 +5,7 @@ from unittest import TestCase, mock, main
import yaml
from pungi.scripts.create_extra_repo import CreateExtraRepo, ExtraRepoInfo
from pungi.scripts.create_extra_repo import CreateExtraRepo, ExtraVariantInfo, RepoInfo
FOLDER_WITH_TEST_DATA = os.path.join(
os.path.dirname(
@ -114,14 +114,17 @@ data:
...
""", Loader=yaml.BaseLoader)
TEST_REPO_INFO = ExtraRepoInfo(
TEST_REPO_INFO = RepoInfo(
path=FOLDER_WITH_TEST_DATA,
folder='test_repo',
is_remote=False,
)
TEST_VARIANT_INFO = ExtraVariantInfo(
name='TestRepo',
arch='x86_64',
is_remote=False,
packages=[],
modules=[],
repos=[TEST_REPO_INFO]
)
BS_BUILD_INFO = {
@ -161,15 +164,19 @@ class TestCreteExtraRepo(TestCase):
)
self.assertEqual(
[
ExtraRepoInfo(
path='https://build.cloudlinux.com/'
f'build_repos/{build_id}/fake_platform',
folder=arch,
ExtraVariantInfo(
name=f'{build_id}-fake_platform-{arch}',
arch=arch,
is_remote=True,
packages=packages,
modules=modules,
repos=[
RepoInfo(
path='https://build.cloudlinux.com/'
f'build_repos/{build_id}/fake_platform',
folder=arch,
is_remote=True,
)
]
)
],
repos_info,
@ -197,7 +204,7 @@ class TestCreteExtraRepo(TestCase):
'CreateExtraRepo._create_local_extra_repo'
) as mock__create_local_extra_repo:
cer = CreateExtraRepo(
repos=[TEST_REPO_INFO],
variants=[TEST_VARIANT_INFO],
bs_auth_token='fake_auth_token',
local_repository_path='/path/to/local/repo',
clear_target_repo=False,

View File

@ -4,7 +4,11 @@ import os
from collections import defaultdict
from unittest import TestCase, mock, main
from pungi.scripts.create_packages_json import PackagesGenerator, RepoInfo
from pungi.scripts.create_packages_json import (
PackagesGenerator,
RepoInfo,
VariantInfo,
)
FOLDER_WITH_TEST_DATA = os.path.join(
os.path.dirname(
@ -16,8 +20,6 @@ FOLDER_WITH_TEST_DATA = os.path.join(
test_repo_info = RepoInfo(
path=FOLDER_WITH_TEST_DATA,
folder='test_repo',
name='TestRepo',
arch='x86_64',
is_remote=False,
is_reference=True,
)
@ -25,11 +27,19 @@ test_repo_info = RepoInfo(
test_repo_info_2 = RepoInfo(
path=FOLDER_WITH_TEST_DATA,
folder='test_repo_2',
name='TestRepo2',
arch='x86_64',
is_remote=False,
is_reference=True,
)
variant_info_1 = VariantInfo(
name='TestRepo',
arch='x86_64',
repos=[test_repo_info]
)
variant_info_2 = VariantInfo(
name='TestRepo2',
arch='x86_64',
repos=[test_repo_info_2]
)
class TestPackagesJson(TestCase):
@ -47,7 +57,12 @@ class TestPackagesJson(TestCase):
'pungi.scripts.create_packages_json.tempfile.NamedTemporaryFile',
) as mock_tempfile:
mock_tempfile.return_value.__enter__.return_value.name = 'tmpfile'
file_name = PackagesGenerator.get_remote_file_content(
packages_generator = PackagesGenerator(
variants=[],
excluded_packages=[],
included_packages=[],
)
file_name = packages_generator.get_remote_file_content(
file_url='fakeurl')
mock_requests_get.assert_called_once_with(url='fakeurl')
mock_tempfile.assert_called_once_with(delete=False)
@ -60,9 +75,9 @@ class TestPackagesJson(TestCase):
def test_02_generate_additional_packages(self):
pg = PackagesGenerator(
repos=[
test_repo_info,
test_repo_info_2,
variants=[
variant_info_1,
variant_info_2,
],
excluded_packages=['zziplib-utils'],
included_packages=['vim-file*'],

View File

@ -2,7 +2,7 @@
import logging
import mock
from unittest import mock
import six
import os
@ -552,6 +552,7 @@ class CreateisoThreadTest(helpers.PungiTestCase):
"release_version": "1.0",
"runroot_tag": "f25-build",
"koji_profile": "koji",
"koji_cache": "/tmp",
},
)
cmd = {
@ -633,6 +634,7 @@ class CreateisoThreadTest(helpers.PungiTestCase):
"release_version": "1.0",
"runroot_tag": "f25-build",
"koji_profile": "koji",
"koji_cache": "/tmp",
"create_jigdo": False,
"runroot_weights": {"createiso": 123},
},
@ -717,6 +719,7 @@ class CreateisoThreadTest(helpers.PungiTestCase):
"buildinstall_method": "lorax",
"runroot_tag": "f25-build",
"koji_profile": "koji",
"koji_cache": "/tmp",
},
)
cmd = {
@ -807,6 +810,7 @@ class CreateisoThreadTest(helpers.PungiTestCase):
"release_version": "1.0",
"runroot_tag": "f25-build",
"koji_profile": "koji",
"koji_cache": "/tmp",
},
)
cmd = {
@ -839,6 +843,7 @@ class CreateisoThreadTest(helpers.PungiTestCase):
"release_version": "1.0",
"runroot_tag": "f25-build",
"koji_profile": "koji",
"koji_cache": "/tmp",
"failable_deliverables": [("^.*$", {"*": "iso"})],
},
)
@ -881,6 +886,7 @@ class CreateisoThreadTest(helpers.PungiTestCase):
"release_version": "1.0",
"runroot_tag": "f25-build",
"koji_profile": "koji",
"koji_cache": "/tmp",
"failable_deliverables": [("^.*$", {"*": "iso"})],
},
)

View File

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
import mock
from unittest import mock
from parameterized import parameterized
import os
from six.moves import StringIO
@ -391,3 +392,27 @@ class CreateIsoScriptTest(helpers.PungiTestCase):
),
]
)
@parameterized.expand(
[("644", 0o644), ("664", 0o664), ("666", 0o666), ("2644", 0o2644)]
)
def test_get_perms_non_executable(self, test_name, mode):
path = helpers.touch(os.path.join(self.topdir, "f"), mode=mode)
self.assertEqual(createiso._get_perms(path), 0o444)
@parameterized.expand(
[
("544", 0o544),
("554", 0o554),
("555", 0o555),
("744", 0o744),
("755", 0o755),
("774", 0o774),
("775", 0o775),
("777", 0o777),
("2775", 0o2775),
]
)
def test_get_perms_executable(self, test_name, mode):
path = helpers.touch(os.path.join(self.topdir, "f"), mode=mode)
self.assertEqual(createiso._get_perms(path), 0o555)

View File

@ -8,7 +8,7 @@ except ImportError:
import glob
import os
import mock
from unittest import mock
import six
from pungi.module_util import Modulemd

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
import mock
from unittest import mock
import os
from productmd.extra_files import ExtraFiles

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
import mock
from unittest import mock
from pungi.phases.gather.methods import method_deps as deps
from tests import helpers

View File

@ -2,7 +2,7 @@
from collections import namedtuple
import copy
import mock
from unittest import mock
import os
import six

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
import mock
from unittest import mock
import os
import six

View File

@ -110,13 +110,13 @@ class TestModulesYamlParser(TestCase):
os.listdir(os.path.join(PATH_TO_KOJI, 'module_defaults')))
# check that modules were exported
self.assertEqual(MARIADB_MODULE, yaml.load(
self.assertEqual(MARIADB_MODULE, yaml.safe_load(
open(os.path.join(PATH_TO_KOJI, 'modules/x86_64', 'mariadb-devel-10.3_1-8010020200108182321.cdc1202b'))))
self.assertEqual(JAVAPACKAGES_TOOLS_MODULE, yaml.load(
self.assertEqual(JAVAPACKAGES_TOOLS_MODULE, yaml.safe_load(
open(os.path.join(PATH_TO_KOJI, 'modules/x86_64', 'javapackages-tools-201801-8000020190628172923.b07bea58'))))
# check that defaults were copied
self.assertEqual(ANT_DEFAULTS, yaml.load(
self.assertEqual(ANT_DEFAULTS, yaml.safe_load(
open(os.path.join(PATH_TO_KOJI, 'module_defaults', 'ant.yaml'))))

View File

@ -4,7 +4,7 @@ import copy
import json
import os
import mock
from unittest import mock
try:
import unittest2 as unittest

View File

@ -6,6 +6,7 @@ from pathlib import Path
from pyfakefs.fake_filesystem_unittest import TestCase
from pungi.scripts.gather_rpms import search_rpms, copy_rpms, Package
from productmd.common import parse_nvra
PATH_TO_REPOS = '/path/to/repos'
MODULES_YAML_GZ = 'modules.yaml.gz'
@ -15,10 +16,13 @@ class TestGatherRpms(TestCase):
maxDiff = None
FILES_TO_CREATE = [
'powertools/Packages/libvirt-6.0.0-28.module_el8.3.0+555+a55c8938.i686.rpm',
'powertools/Packages/libvirt-6.0.0-28.module_el'
'8.3.0+555+a55c8938.i686.rpm',
'powertools/Packages/libgit2-devel-0.26.8-2.el8.x86_64.rpm',
'powertools/Packages/xalan-j2-2.7.1-38.module_el8.0.0+30+832da3a1.noarch.rpm',
'appstream/Packages/bnd-maven-plugin-3.5.0-4.module_el8.0.0+30+832da3a1.noarch.rpm',
'powertools/Packages/xalan-j2-2.7.1-38.module_el'
'8.0.0+30+832da3a1.noarch.rpm',
'appstream/Packages/bnd-maven-plugin-3.5.0-4.module_el'
'8.0.0+30+832da3a1.noarch.rpm',
'appstream/Packages/OpenEXR-devel-2.2.0-11.el8.i686.rpm',
'appstream/Packages/mingw-binutils-generic-2.30-1.el8.x86_64.rpm',
'appstream/Packages/somenonrpm',
@ -30,56 +34,98 @@ class TestGatherRpms(TestCase):
os.makedirs(PATH_TO_REPOS)
for filepath in self.FILES_TO_CREATE:
os.makedirs(os.path.join(PATH_TO_REPOS, os.path.dirname(filepath)), exist_ok=True)
os.makedirs(
os.path.join(PATH_TO_REPOS, os.path.dirname(filepath)),
exist_ok=True,
)
open(os.path.join(PATH_TO_REPOS, filepath), 'w').close()
def test_gather_rpms(self):
self.assertEqual(
[Package(nvra='libvirt-6.0.0-28.module_el8.3.0+555+a55c8938.i686',
path=f'{PATH_TO_REPOS}/powertools/Packages/'
f'libvirt-6.0.0-28.module_el8.3.0+555+a55c8938.i686.rpm'),
Package(nvra='libgit2-devel-0.26.8-2.el8.x86_64',
path=f'{PATH_TO_REPOS}/powertools/Packages/'
f'libgit2-devel-0.26.8-2.el8.x86_64.rpm'),
Package(nvra='xalan-j2-2.7.1-38.module_el8.0.0+30+832da3a1.noarch',
path=f'{PATH_TO_REPOS}/powertools/Packages/'
f'xalan-j2-2.7.1-38.module_el8.0.0+30+832da3a1.noarch.rpm'),
Package(nvra='bnd-maven-plugin-3.5.0-4.module_el8.0.0+30+832da3a1.noarch',
path='/path/to/repos/appstream/Packages/'
'bnd-maven-plugin-3.5.0-4.module_el8.0.0+30+832da3a1.noarch.rpm'),
Package(nvra='OpenEXR-devel-2.2.0-11.el8.i686',
path=f'{PATH_TO_REPOS}/appstream/Packages/'
f'OpenEXR-devel-2.2.0-11.el8.i686.rpm'),
Package(nvra='mingw-binutils-generic-2.30-1.el8.x86_64',
path=f'{PATH_TO_REPOS}/appstream/Packages/'
f'mingw-binutils-generic-2.30-1.el8.x86_64.rpm')],
search_rpms(PATH_TO_REPOS)
[Package(nvra=parse_nvra('libvirt-6.0.0-28.module_'
'el8.3.0+555+a55c8938.i686'),
path=Path(
f'{PATH_TO_REPOS}/powertools/Packages/'
f'libvirt-6.0.0-28.module_el'
f'8.3.0+555+a55c8938.i686.rpm'
)),
Package(nvra=parse_nvra('libgit2-devel-0.26.8-2.el8.x86_64'),
path=Path(
f'{PATH_TO_REPOS}/powertools/Packages/'
f'libgit2-devel-0.26.8-2.el8.x86_64.rpm'
)),
Package(nvra=parse_nvra('xalan-j2-2.7.1-38.module_el'
'8.0.0+30+832da3a1.noarch'),
path=Path(
f'{PATH_TO_REPOS}/powertools/Packages/'
f'xalan-j2-2.7.1-38.module_el'
f'8.0.0+30+832da3a1.noarch.rpm'
)),
Package(nvra=parse_nvra('bnd-maven-plugin-3.5.0-4.module_el'
'8.0.0+30+832da3a1.noarch'),
path=Path(
'/path/to/repos/appstream/Packages/'
'bnd-maven-plugin-3.5.0-4.module_el'
'8.0.0+30+832da3a1.noarch.rpm'
)),
Package(nvra=parse_nvra('OpenEXR-devel-2.2.0-11.el8.i686'),
path=Path(
f'{PATH_TO_REPOS}/appstream/Packages/'
f'OpenEXR-devel-2.2.0-11.el8.i686.rpm'
)),
Package(nvra=parse_nvra('mingw-binutils-generic-'
'2.30-1.el8.x86_64'),
path=Path(
f'{PATH_TO_REPOS}/appstream/Packages/'
f'mingw-binutils-generic-2.30-1.el8.x86_64.rpm'
))
],
search_rpms(Path(PATH_TO_REPOS))
)
def test_copy_rpms(self):
target_path = Path('/mnt/koji')
packages = [
Package(nvra='libvirt-6.0.0-28.module_el8.3.0+555+a55c8938.i686',
path=f'{PATH_TO_REPOS}/powertools/Packages/'
f'libvirt-6.0.0-28.module_el8.3.0+555+a55c8938.i686.rpm'),
Package(nvra='libgit2-devel-0.26.8-2.el8.x86_64',
path=f'{PATH_TO_REPOS}/powertools/Packages/'
f'libgit2-devel-0.26.8-2.el8.x86_64.rpm'),
Package(nvra='xalan-j2-2.7.1-38.module_el8.0.0+30+832da3a1.noarch',
path=f'{PATH_TO_REPOS}/powertools/Packages/'
f'xalan-j2-2.7.1-38.module_el8.0.0+30+832da3a1.noarch.rpm'),
Package(nvra='bnd-maven-plugin-3.5.0-4.module_el8.0.0+30+832da3a1.noarch',
path='/path/to/repos/appstream/Packages/'
'bnd-maven-plugin-3.5.0-4.module_el8.0.0+30+832da3a1.noarch.rpm'),
Package(nvra='OpenEXR-devel-2.2.0-11.el8.i686',
path=f'{PATH_TO_REPOS}/appstream/Packages/'
f'OpenEXR-devel-2.2.0-11.el8.i686.rpm'),
Package(nvra='mingw-binutils-generic-2.30-1.el8.x86_64',
path=f'{PATH_TO_REPOS}/appstream/Packages/'
f'mingw-binutils-generic-2.30-1.el8.x86_64.rpm')
Package(nvra=parse_nvra('libvirt-6.0.0-28.module_'
'el8.3.0+555+a55c8938.i686'),
path=Path(
f'{PATH_TO_REPOS}/powertools/Packages/'
f'libvirt-6.0.0-28.module_el'
f'8.3.0+555+a55c8938.i686.rpm'
)),
Package(nvra=parse_nvra('libgit2-devel-0.26.8-2.el8.x86_64'),
path=Path(
f'{PATH_TO_REPOS}/powertools/Packages/'
f'libgit2-devel-0.26.8-2.el8.x86_64.rpm'
)),
Package(nvra=parse_nvra('xalan-j2-2.7.1-38.module_'
'el8.0.0+30+832da3a1.noarch'),
path=Path(
f'{PATH_TO_REPOS}/powertools/Packages/'
f'xalan-j2-2.7.1-38.module_el'
f'8.0.0+30+832da3a1.noarch.rpm'
)),
Package(nvra=parse_nvra('bnd-maven-plugin-3.5.0-4.module_el'
'8.0.0+30+832da3a1.noarch'),
path=Path(
'/path/to/repos/appstream/Packages/'
'bnd-maven-plugin-3.5.0-4.module_el'
'8.0.0+30+832da3a1.noarch.rpm'
)),
Package(nvra=parse_nvra('OpenEXR-devel-2.2.0-11.el8.i686'),
path=Path(
f'{PATH_TO_REPOS}/appstream/Packages/'
f'OpenEXR-devel-2.2.0-11.el8.i686.rpm'
)),
Package(nvra=parse_nvra('mingw-binutils-generic-'
'2.30-1.el8.x86_64'),
path=Path(
f'{PATH_TO_REPOS}/appstream/Packages/'
f'mingw-binutils-generic-2.30-1.el8.x86_64.rpm'
))
]
copy_rpms(packages, target_path)
copy_rpms(packages, target_path, [])
self.assertCountEqual([
'xalan-j2-2.7.1-38.module_el8.0.0+30+832da3a1.noarch.rpm',

View File

@ -5,7 +5,7 @@ try:
except ImportError:
import unittest
import mock
from unittest import mock
import six
from pungi.phases.gather.sources.source_module import GatherSourceModule

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
import mock
from unittest import mock
import os
@ -122,6 +122,7 @@ class ImageContainerThreadTest(helpers.PungiTestCase):
self.topdir,
{
"koji_profile": "koji",
"koji_cache": "/tmp",
"translate_paths": [(self.topdir, "http://root")],
},
)

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
import mock
from unittest import mock
import six
@ -35,6 +35,7 @@ class TestImageBuildPhase(PungiTestCase):
{
"image_build": {"^Client|Server$": [original_image_conf]},
"koji_profile": "koji",
"koji_cache": "/tmp",
},
)
@ -45,7 +46,7 @@ class TestImageBuildPhase(PungiTestCase):
phase.run()
# assert at least one thread was started
self.assertTrue(phase.pool.add.called)
phase.pool.add.assert_called()
client_args = {
"original_image_conf": original_image_conf,
"image_conf": {
@ -127,6 +128,7 @@ class TestImageBuildPhase(PungiTestCase):
"image_build_version": "Rawhide",
"image_build": {"^Server$": [original_image_conf]},
"koji_profile": "koji",
"koji_cache": "/tmp",
},
)
@ -137,7 +139,7 @@ class TestImageBuildPhase(PungiTestCase):
phase.run()
# assert at least one thread was started
self.assertTrue(phase.pool.add.called)
phase.pool.add.assert_called()
server_args = {
"original_image_conf": original_image_conf,
"image_conf": {
@ -188,6 +190,7 @@ class TestImageBuildPhase(PungiTestCase):
"image_build_target": "f24",
"image_build": {"^Server$": [original_image_conf]},
"koji_profile": "koji",
"koji_cache": "/tmp",
},
)
@ -196,7 +199,7 @@ class TestImageBuildPhase(PungiTestCase):
phase.run()
# assert at least one thread was started
self.assertTrue(phase.pool.add.called)
phase.pool.add.assert_called()
server_args = {
"original_image_conf": original_image_conf,
"image_conf": {
@ -251,6 +254,7 @@ class TestImageBuildPhase(PungiTestCase):
]
},
"koji_profile": "koji",
"koji_cache": "/tmp",
},
)
@ -261,8 +265,8 @@ class TestImageBuildPhase(PungiTestCase):
phase.run()
# assert at least one thread was started
self.assertFalse(phase.pool.add.called)
self.assertFalse(phase.pool.queue_put.called)
phase.pool.add.assert_not_called()
phase.pool.queue_put.assert_not_called()
@mock.patch("pungi.phases.image_build.ThreadPool")
def test_image_build_set_install_tree(self, ThreadPool):
@ -286,6 +290,7 @@ class TestImageBuildPhase(PungiTestCase):
{
"image_build": {"^Server$": [original_image_conf]},
"koji_profile": "koji",
"koji_cache": "/tmp",
},
)
compose.setup_optional()
@ -297,9 +302,9 @@ class TestImageBuildPhase(PungiTestCase):
phase.run()
# assert at least one thread was started
self.assertTrue(phase.pool.add.called)
phase.pool.add.assert_called()
self.assertTrue(phase.pool.queue_put.called_once)
phase.pool.queue_put.assert_called_once()
args, kwargs = phase.pool.queue_put.call_args
self.assertEqual(args[0][0], compose)
self.assertDictEqual(
@ -353,6 +358,7 @@ class TestImageBuildPhase(PungiTestCase):
{
"image_build": {"^Server$": [original_image_conf]},
"koji_profile": "koji",
"koji_cache": "/tmp",
"translate_paths": [("/my", "http://example.com")],
},
)
@ -364,9 +370,9 @@ class TestImageBuildPhase(PungiTestCase):
phase.run()
# assert at least one thread was started
self.assertTrue(phase.pool.add.called)
phase.pool.add.assert_called()
self.assertTrue(phase.pool.queue_put.called_once)
phase.pool.queue_put.assert_called_once()
args, kwargs = phase.pool.queue_put.call_args
self.assertEqual(args[0][0], compose)
self.assertDictEqual(
@ -419,6 +425,7 @@ class TestImageBuildPhase(PungiTestCase):
{
"image_build": {"^Server$": [original_image_conf]},
"koji_profile": "koji",
"koji_cache": "/tmp",
},
)
compose.setup_optional()
@ -430,9 +437,9 @@ class TestImageBuildPhase(PungiTestCase):
phase.run()
# assert at least one thread was started
self.assertTrue(phase.pool.add.called)
phase.pool.add.assert_called()
self.assertTrue(phase.pool.queue_put.called_once)
phase.pool.queue_put.assert_called_once()
args, kwargs = phase.pool.queue_put.call_args
self.assertEqual(args[0][0], compose)
self.assertDictEqual(
@ -491,6 +498,7 @@ class TestImageBuildPhase(PungiTestCase):
{
"image_build": {"^Server$": [original_image_conf]},
"koji_profile": "koji",
"koji_cache": "/tmp",
},
)
@ -501,9 +509,9 @@ class TestImageBuildPhase(PungiTestCase):
phase.run()
# assert at least one thread was started
self.assertTrue(phase.pool.add.called)
phase.pool.add.assert_called()
self.assertTrue(phase.pool.queue_put.called_once)
phase.pool.queue_put.assert_called_once()
args, kwargs = phase.pool.queue_put.call_args
self.assertEqual(args[0][0], compose)
self.assertDictEqual(
@ -559,6 +567,7 @@ class TestImageBuildPhase(PungiTestCase):
]
},
"koji_profile": "koji",
"koji_cache": "/tmp",
},
)
@ -569,9 +578,9 @@ class TestImageBuildPhase(PungiTestCase):
phase.run()
# assert at least one thread was started
self.assertTrue(phase.pool.add.called)
phase.pool.add.assert_called()
self.assertTrue(phase.pool.queue_put.called_once)
phase.pool.queue_put.assert_called_once()
args, kwargs = phase.pool.queue_put.call_args
self.assertEqual(
args[0][1].get("image_conf", {}).get("image-build", {}).get("release"),
@ -602,6 +611,7 @@ class TestImageBuildPhase(PungiTestCase):
]
},
"koji_profile": "koji",
"koji_cache": "/tmp",
},
)
@ -612,9 +622,9 @@ class TestImageBuildPhase(PungiTestCase):
phase.run()
# assert at least one thread was started
self.assertTrue(phase.pool.add.called)
phase.pool.add.assert_called()
self.assertTrue(phase.pool.queue_put.called_once)
phase.pool.queue_put.assert_called_once()
args, kwargs = phase.pool.queue_put.call_args
self.assertEqual(
args[0][1].get("image_conf", {}).get("image-build", {}).get("release"),
@ -645,6 +655,7 @@ class TestImageBuildPhase(PungiTestCase):
]
},
"koji_profile": "koji",
"koji_cache": "/tmp",
},
)
@ -655,9 +666,9 @@ class TestImageBuildPhase(PungiTestCase):
phase.run()
# assert at least one thread was started
self.assertTrue(phase.pool.add.called)
phase.pool.add.assert_called()
self.assertTrue(phase.pool.queue_put.called_once)
phase.pool.queue_put.assert_called_once()
args, kwargs = phase.pool.queue_put.call_args
self.assertTrue(args[0][1].get("scratch"))
@ -681,6 +692,7 @@ class TestImageBuildPhase(PungiTestCase):
{
"image_build": {"^Server-optional$": [original_image_conf]},
"koji_profile": "koji",
"koji_cache": "/tmp",
},
)
compose.setup_optional()
@ -692,7 +704,7 @@ class TestImageBuildPhase(PungiTestCase):
phase.run()
# assert at least one thread was started
self.assertTrue(phase.pool.add.called)
phase.pool.add.assert_called()
server_args = {
"original_image_conf": original_image_conf,
"image_conf": {
@ -744,6 +756,7 @@ class TestImageBuildPhase(PungiTestCase):
{
"image_build": {"^Server$": [original_image_conf]},
"koji_profile": "koji",
"koji_cache": "/tmp",
},
)
compose.setup_optional()
@ -755,7 +768,7 @@ class TestImageBuildPhase(PungiTestCase):
phase.run()
# assert at least one thread was started
self.assertTrue(phase.pool.add.called)
phase.pool.add.assert_called()
server_args = {
"original_image_conf": original_image_conf,
"image_conf": {
@ -943,7 +956,9 @@ class TestCreateImageBuildThread(PungiTestCase):
@mock.patch("pungi.phases.image_build.KojiWrapper")
@mock.patch("pungi.phases.image_build.Linker")
def test_process_handle_fail(self, Linker, KojiWrapper):
compose = DummyCompose(self.topdir, {"koji_profile": "koji"})
compose = DummyCompose(
self.topdir, {"koji_profile": "koji", "koji_cache": "/tmp"}
)
pool = mock.Mock()
cmd = {
"image_conf": {
@ -1000,7 +1015,9 @@ class TestCreateImageBuildThread(PungiTestCase):
@mock.patch("pungi.phases.image_build.KojiWrapper")
@mock.patch("pungi.phases.image_build.Linker")
def test_process_handle_exception(self, Linker, KojiWrapper):
compose = DummyCompose(self.topdir, {"koji_profile": "koji"})
compose = DummyCompose(
self.topdir, {"koji_profile": "koji", "koji_cache": "/tmp"}
)
pool = mock.Mock()
cmd = {
"image_conf": {
@ -1046,7 +1063,9 @@ class TestCreateImageBuildThread(PungiTestCase):
@mock.patch("pungi.phases.image_build.KojiWrapper")
@mock.patch("pungi.phases.image_build.Linker")
def test_process_handle_fail_only_one_optional(self, Linker, KojiWrapper):
compose = DummyCompose(self.topdir, {"koji_profile": "koji"})
compose = DummyCompose(
self.topdir, {"koji_profile": "koji", "koji_cache": "/tmp"}
)
pool = mock.Mock()
cmd = {
"image_conf": {

View File

@ -4,7 +4,7 @@ try:
import unittest2 as unittest
except ImportError:
import unittest
import mock
from unittest import mock
import os
import tempfile

View File

@ -5,7 +5,7 @@ try:
import unittest2 as unittest
except ImportError:
import unittest
import mock
from unittest import mock
import six

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
import itertools
import mock
from unittest import mock
import os
import six
@ -28,6 +28,7 @@ def fake_listdir(pattern, result=None, exc=None):
"""Create a function that mocks os.listdir. If the path contains pattern,
result will be returned or exc raised. Otherwise it's normal os.listdir
"""
# The point of this is to avoid issues on Python 2, where apparently
# isdir() is using listdir(), so the mocking is breaking it.
def worker(path):

View File

@ -117,7 +117,11 @@ version: '1'
os.makedirs(os.path.join(PATH_TO_REPOS, os.path.dirname(filepath)), exist_ok=True)
open(os.path.join(PATH_TO_REPOS, filepath), 'w').close()
self._koji = KojiMock(PATH_TO_REPOS, os.path.join(PATH_TO_REPOS, 'modules'))
self._koji = KojiMock(
PATH_TO_REPOS,
os.path.join(PATH_TO_REPOS, 'modules'),
['x86_64', 'noarch', 'i686'],
)
@ddt.data(
[0, {
@ -331,6 +335,7 @@ version: '1'
result = self._koji.listTagged('dist-c8-module-compose')
self.assertEqual([
{
'arch': 'x86_64',
'build_id': 0,
'id': 0,
'name': 'javapackages-tools',
@ -342,6 +347,7 @@ version: '1'
'version': '201801'
},
{
'arch': 'x86_64',
'build_id': 1,
'id': 1,
'name': 'mariadb-devel',

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
import json
import mock
from unittest import mock
try:
import unittest2 as unittest
@ -121,7 +121,6 @@ class KojiWrapperTest(KojiWrapperBaseTestCase):
)
def test_get_image_paths(self):
# The data for this tests is obtained from the actual Koji build. It
# includes lots of fields that are not used, but for the sake of
# completeness is fully preserved.
@ -321,7 +320,6 @@ class KojiWrapperTest(KojiWrapperBaseTestCase):
)
def test_get_image_paths_failed_subtask(self):
failed = set()
def failed_callback(arch):

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
import mock
from unittest import mock
import errno
import os
import stat

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
import mock
from unittest import mock
import six
@ -43,7 +43,7 @@ class TestLiveImagesPhase(PungiTestCase):
phase.run()
# assert at least one thread was started
self.assertTrue(phase.pool.add.called)
phase.pool.add.assert_called()
self.maxDiff = None
six.assertCountEqual(
self,
@ -124,7 +124,7 @@ class TestLiveImagesPhase(PungiTestCase):
phase.run()
# assert at least one thread was started
self.assertTrue(phase.pool.add.called)
phase.pool.add.assert_called()
self.maxDiff = None
six.assertCountEqual(
self,
@ -192,7 +192,7 @@ class TestLiveImagesPhase(PungiTestCase):
phase.run()
# assert at least one thread was started
self.assertTrue(phase.pool.add.called)
phase.pool.add.assert_called()
self.maxDiff = None
six.assertCountEqual(
self,
@ -265,7 +265,7 @@ class TestLiveImagesPhase(PungiTestCase):
phase.run()
# assert at least one thread was started
self.assertTrue(phase.pool.add.called)
phase.pool.add.assert_called()
self.maxDiff = None
six.assertCountEqual(
self,
@ -363,7 +363,7 @@ class TestLiveImagesPhase(PungiTestCase):
phase.run()
# assert at least one thread was started
self.assertTrue(phase.pool.add.called)
phase.pool.add.assert_called()
self.maxDiff = None
six.assertCountEqual(
self,
@ -433,7 +433,7 @@ class TestLiveImagesPhase(PungiTestCase):
phase.run()
# assert at least one thread was started
self.assertTrue(phase.pool.add.called)
phase.pool.add.assert_called()
self.maxDiff = None
six.assertCountEqual(
self,
@ -503,7 +503,7 @@ class TestLiveImagesPhase(PungiTestCase):
phase.run()
# assert at least one thread was started
self.assertTrue(phase.pool.add.called)
phase.pool.add.assert_called()
self.maxDiff = None
six.assertCountEqual(
self,
@ -571,7 +571,7 @@ class TestLiveImagesPhase(PungiTestCase):
phase.run()
# assert at least one thread was started
self.assertTrue(phase.pool.add.called)
phase.pool.add.assert_called()
self.maxDiff = None
six.assertCountEqual(
self,
@ -958,7 +958,9 @@ class TestCreateLiveImageThread(PungiTestCase):
@mock.patch("pungi.phases.live_images.run")
@mock.patch("pungi.phases.live_images.KojiWrapper")
def test_process_handles_fail(self, KojiWrapper, run, copy2):
compose = DummyCompose(self.topdir, {"koji_profile": "koji"})
compose = DummyCompose(
self.topdir, {"koji_profile": "koji", "koji_cache": "/tmp"}
)
pool = mock.Mock()
cmd = {
"ks_file": "/path/to/ks_file",
@ -1011,7 +1013,9 @@ class TestCreateLiveImageThread(PungiTestCase):
@mock.patch("pungi.phases.live_images.run")
@mock.patch("pungi.phases.live_images.KojiWrapper")
def test_process_handles_exception(self, KojiWrapper, run, copy2):
compose = DummyCompose(self.topdir, {"koji_profile": "koji"})
compose = DummyCompose(
self.topdir, {"koji_profile": "koji", "koji_cache": "/tmp"}
)
pool = mock.Mock()
cmd = {
"ks_file": "/path/to/ks_file",

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
import mock
from unittest import mock
import os
@ -28,6 +28,7 @@ class TestLiveMediaPhase(PungiTestCase):
]
},
"koji_profile": "koji",
"koji_cache": "/tmp",
},
)
@ -36,7 +37,7 @@ class TestLiveMediaPhase(PungiTestCase):
phase = LiveMediaPhase(compose)
phase.run()
self.assertTrue(phase.pool.add.called)
phase.pool.add.assert_called()
self.assertEqual(
phase.pool.queue_put.call_args_list,
[
@ -85,6 +86,7 @@ class TestLiveMediaPhase(PungiTestCase):
]
},
"koji_profile": "koji",
"koji_cache": "/tmp",
},
)
@ -93,7 +95,7 @@ class TestLiveMediaPhase(PungiTestCase):
phase = LiveMediaPhase(compose)
phase.run()
self.assertTrue(phase.pool.add.called)
phase.pool.add.assert_called()
self.assertEqual(
phase.pool.queue_put.call_args_list,
[
@ -148,6 +150,7 @@ class TestLiveMediaPhase(PungiTestCase):
]
},
"koji_profile": "koji",
"koji_cache": "/tmp",
},
)
@ -156,7 +159,7 @@ class TestLiveMediaPhase(PungiTestCase):
phase = LiveMediaPhase(compose)
phase.run()
self.assertTrue(phase.pool.add.called)
phase.pool.add.assert_called()
self.assertEqual(
phase.pool.queue_put.call_args_list,
[
@ -259,6 +262,7 @@ class TestLiveMediaPhase(PungiTestCase):
]
},
"koji_profile": "koji",
"koji_cache": "/tmp",
},
)
@ -267,7 +271,7 @@ class TestLiveMediaPhase(PungiTestCase):
phase = LiveMediaPhase(compose)
phase.run()
self.assertTrue(phase.pool.add.called)
phase.pool.add.assert_called()
self.assertEqual(
phase.pool.queue_put.call_args_list,
[
@ -364,6 +368,7 @@ class TestLiveMediaPhase(PungiTestCase):
]
},
"koji_profile": "koji",
"koji_cache": "/tmp",
},
)
@ -394,6 +399,7 @@ class TestLiveMediaPhase(PungiTestCase):
]
},
"koji_profile": "koji",
"koji_cache": "/tmp",
},
)
@ -444,7 +450,7 @@ class TestLiveMediaPhase(PungiTestCase):
phase = LiveMediaPhase(compose)
phase.run()
self.assertTrue(phase.pool.add.called)
phase.pool.add.assert_called()
self.assertEqual(
phase.pool.queue_put.call_args_list,
@ -611,7 +617,9 @@ class TestLiveMediaThread(PungiTestCase):
@mock.patch("pungi.phases.livemedia_phase.get_file_size")
@mock.patch("pungi.phases.livemedia_phase.KojiWrapper")
def test_handle_koji_fail(self, KojiWrapper, get_file_size, get_mtime):
compose = DummyCompose(self.topdir, {"koji_profile": "koji"})
compose = DummyCompose(
self.topdir, {"koji_profile": "koji", "koji_cache": "/tmp"}
)
config = {
"arches": ["amd64", "x86_64"],
"ksfile": "file.ks",
@ -688,6 +696,7 @@ class TestLiveMediaThread(PungiTestCase):
self.topdir,
{
"koji_profile": "koji",
"koji_cache": "/tmp",
"failable_deliverables": [("^.+$", {"*": ["live-media"]})],
},
)
@ -757,6 +766,7 @@ class TestLiveMediaThread(PungiTestCase):
self.topdir,
{
"koji_profile": "koji",
"koji_cache": "/tmp",
"failable_deliverables": [("^.+$", {"*": ["live-media"]})],
},
)

View File

@ -4,7 +4,7 @@ try:
import unittest2 as unittest
except ImportError:
import unittest
import mock
from unittest import mock
from pungi import media_split

View File

@ -1,4 +1,4 @@
import mock
from unittest import mock
import os
import six

View File

@ -2,7 +2,7 @@
from datetime import datetime
import json
import mock
from unittest import mock
try:
import unittest2 as unittest
@ -133,7 +133,7 @@ class TestNotifier(unittest.TestCase):
def test_does_not_run_without_config(self, run, makedirs):
n = PungiNotifier(None)
n.send("cmd", foo="bar", baz="quux")
self.assertFalse(run.called)
run.assert_not_called()
@mock.patch("pungi.util.translate_path")
@mock.patch("kobo.shortcuts.run")
@ -146,4 +146,4 @@ class TestNotifier(unittest.TestCase):
n.send("cmd", **self.data)
self.assertEqual(run.call_args_list, [self._call("run-notify", "cmd")])
self.assertTrue(self.compose.log_warning.called)
self.compose.log_warning.assert_called()

View File

@ -1,934 +0,0 @@
# -*- coding: utf-8 -*-
import itertools
import json
from functools import wraps
import operator
import os
import shutil
import subprocess
from textwrap import dedent
import mock
import six
from six.moves import configparser
from parameterized import parameterized
from tests.helpers import BaseTestCase, PungiTestCase, touch, FIXTURE_DIR
from pungi_utils import orchestrator as o
class TestConfigSubstitute(PungiTestCase):
def setUp(self):
super(TestConfigSubstitute, self).setUp()
self.fp = os.path.join(self.topdir, "config.conf")
@parameterized.expand(
[
("hello = 'world'", "hello = 'world'"),
("hello = '{{foo}}'", "hello = 'bar'"),
("hello = '{{ foo}}'", "hello = 'bar'"),
("hello = '{{foo }}'", "hello = 'bar'"),
]
)
def test_substitutions(self, initial, expected):
touch(self.fp, initial)
o.fill_in_config_file(self.fp, {"foo": "bar"})
with open(self.fp) as f:
self.assertEqual(expected, f.read())
def test_missing_key(self):
touch(self.fp, "hello = '{{unknown}}'")
with self.assertRaises(RuntimeError) as ctx:
o.fill_in_config_file(self.fp, {})
self.assertEqual(
"Unknown placeholder 'unknown' in config.conf", str(ctx.exception)
)
class TestSafeGetList(BaseTestCase):
@parameterized.expand(
[
("", []),
("foo", ["foo"]),
("foo,bar", ["foo", "bar"]),
("foo bar", ["foo", "bar"]),
]
)
def test_success(self, value, expected):
cf = configparser.RawConfigParser()
cf.add_section("general")
cf.set("general", "key", value)
self.assertEqual(o._safe_get_list(cf, "general", "key"), expected)
def test_default(self):
cf = configparser.RawConfigParser()
cf.add_section("general")
self.assertEqual(o._safe_get_list(cf, "general", "missing", "hello"), "hello")
class TestComposePart(PungiTestCase):
def test_from_minimal_config(self):
cf = configparser.RawConfigParser()
cf.add_section("test")
cf.set("test", "config", "my.conf")
part = o.ComposePart.from_config(cf, "test", "/tmp/config")
deps = "set()" if six.PY3 else "set([])"
self.assertEqual(str(part), "test")
self.assertEqual(
repr(part),
"ComposePart('test', '/tmp/config/my.conf', 'READY', "
"just_phase=[], skip_phase=[], dependencies=%s)" % deps,
)
self.assertFalse(part.failable)
def test_from_full_config(self):
cf = configparser.RawConfigParser()
cf.add_section("test")
cf.set("test", "config", "my.conf")
cf.set("test", "depends_on", "base")
cf.set("test", "skip_phase", "skip")
cf.set("test", "just_phase", "just")
cf.set("test", "failable", "yes")
part = o.ComposePart.from_config(cf, "test", "/tmp/config")
deps = "{'base'}" if six.PY3 else "set(['base'])"
self.assertEqual(
repr(part),
"ComposePart('test', '/tmp/config/my.conf', 'WAITING', "
"just_phase=['just'], skip_phase=['skip'], dependencies=%s)" % deps,
)
self.assertTrue(part.failable)
def test_get_cmd(self):
conf = o.Config(
"/tgt/", "production", "RC-1.0", "/old", "/cfg", 1234, ["--quiet"]
)
part = o.ComposePart(
"test", "/tmp/my.conf", just_phase=["just"], skip_phase=["skip"]
)
part.path = "/compose"
self.assertEqual(
part.get_cmd(conf),
[
"pungi-koji",
"--config",
"/tmp/my.conf",
"--compose-dir",
"/compose",
"--production",
"--label",
"RC-1.0",
"--just-phase",
"just",
"--skip-phase",
"skip",
"--old-compose",
"/old/parts",
"--koji-event",
"1234",
"--quiet",
"--no-latest-link",
],
)
def test_refresh_status(self):
part = o.ComposePart("test", "/tmp/my.conf")
part.path = os.path.join(self.topdir)
touch(os.path.join(self.topdir, "STATUS"), "FINISHED")
part.refresh_status()
self.assertEqual(part.status, "FINISHED")
def test_refresh_status_missing_file(self):
part = o.ComposePart("test", "/tmp/my.conf")
part.path = os.path.join(self.topdir)
part.refresh_status()
self.assertEqual(part.status, "DOOMED")
@parameterized.expand(["FINISHED", "FINISHED_INCOMPLETE"])
def test_is_finished(self, status):
part = o.ComposePart("test", "/tmp/my.conf")
part.status = status
self.assertTrue(part.is_finished())
@parameterized.expand(["STARTED", "WAITING"])
def test_is_not_finished(self, status):
part = o.ComposePart("test", "/tmp/my.conf")
part.status = status
self.assertFalse(part.is_finished())
@mock.patch("pungi_utils.orchestrator.fill_in_config_file")
@mock.patch("pungi_utils.orchestrator.get_compose_dir")
@mock.patch("kobo.conf.PyConfigParser")
def test_setup_start(self, Conf, gcd, ficf):
def pth(*path):
return os.path.join(self.topdir, *path)
conf = o.Config(
pth("tgt"), "production", "RC-1.0", "/old", pth("cfg"), None, None
)
part = o.ComposePart("test", "/tmp/my.conf")
parts = {"base": mock.Mock(path="/base", is_finished=lambda: True)}
Conf.return_value.opened_files = ["foo.conf"]
part.setup_start(conf, parts)
self.assertEqual(part.status, "STARTED")
self.assertEqual(part.path, gcd.return_value)
self.assertEqual(part.log_file, pth("tgt", "logs", "test.log"))
self.assertEqual(
ficf.call_args_list,
[mock.call("foo.conf", {"part-base": "/base", "configdir": pth("cfg")})],
)
self.assertEqual(
gcd.call_args_list,
[
mock.call(
pth("tgt/parts"),
Conf.return_value,
compose_type="production",
compose_label="RC-1.0",
)
],
)
@parameterized.expand(
[
# Nothing blocking, no change
([], [], o.Status.READY),
# Remove last blocker and switch to READY
(["finished"], [], o.Status.READY),
# Blocker remaining, stay in WAITING
(["finished", "block"], ["block"], o.Status.WAITING),
]
)
def test_unblock_on(self, deps, blockers, status):
part = o.ComposePart("test", "/tmp/my.conf", dependencies=deps)
part.unblock_on("finished")
six.assertCountEqual(self, part.blocked_on, blockers)
self.assertEqual(part.status, status)
class TestStartPart(PungiTestCase):
@mock.patch("subprocess.Popen")
def test_start(self, Popen):
part = mock.Mock(log_file=os.path.join(self.topdir, "log"))
config = mock.Mock()
parts = mock.Mock()
cmd = ["pungi-koji", "..."]
part.get_cmd.return_value = cmd
proc = o.start_part(config, parts, part)
self.assertEqual(
part.mock_calls,
[mock.call.setup_start(config, parts), mock.call.get_cmd(config)],
)
self.assertEqual(proc, Popen.return_value)
self.assertEqual(
Popen.call_args_list,
[mock.call(cmd, stdout=mock.ANY, stderr=subprocess.STDOUT)],
)
class TestHandleFinished(BaseTestCase):
def setUp(self):
self.config = mock.Mock()
self.linker = mock.Mock()
self.parts = {"a": mock.Mock(), "b": mock.Mock()}
@mock.patch("pungi_utils.orchestrator.update_metadata")
@mock.patch("pungi_utils.orchestrator.copy_part")
def test_handle_success(self, cp, um):
proc = mock.Mock(returncode=0)
o.handle_finished(self.config, self.linker, self.parts, proc, self.parts["a"])
self.assertEqual(
self.parts["a"].mock_calls,
[mock.call.refresh_status(), mock.call.unblock_on(self.parts["a"].name)],
)
self.assertEqual(
self.parts["b"].mock_calls, [mock.call.unblock_on(self.parts["a"].name)]
)
self.assertEqual(
cp.call_args_list, [mock.call(self.config, self.linker, self.parts["a"])]
)
self.assertEqual(um.call_args_list, [mock.call(self.config, self.parts["a"])])
@mock.patch("pungi_utils.orchestrator.block_on")
def test_handle_failure(self, bo):
proc = mock.Mock(returncode=1)
o.handle_finished(self.config, self.linker, self.parts, proc, self.parts["a"])
self.assertEqual(self.parts["a"].mock_calls, [mock.call.refresh_status()])
self.assertEqual(
bo.call_args_list, [mock.call(self.parts, self.parts["a"].name)]
)
class TestBlockOn(BaseTestCase):
def test_single(self):
parts = {"b": o.ComposePart("b", "b.conf", dependencies=["a"])}
o.block_on(parts, "a")
self.assertEqual(parts["b"].status, o.Status.BLOCKED)
def test_chain(self):
parts = {
"b": o.ComposePart("b", "b.conf", dependencies=["a"]),
"c": o.ComposePart("c", "c.conf", dependencies=["b"]),
"d": o.ComposePart("d", "d.conf", dependencies=["c"]),
}
o.block_on(parts, "a")
self.assertEqual(parts["b"].status, o.Status.BLOCKED)
self.assertEqual(parts["c"].status, o.Status.BLOCKED)
self.assertEqual(parts["d"].status, o.Status.BLOCKED)
class TestUpdateMetadata(PungiTestCase):
def assertEqualJSON(self, f1, f2):
with open(f1) as f:
actual = json.load(f)
with open(f2) as f:
expected = json.load(f)
self.assertEqual(actual, expected)
def assertEqualMetadata(self, expected):
expected_dir = os.path.join(FIXTURE_DIR, expected, "compose/metadata")
for f in os.listdir(expected_dir):
self.assertEqualJSON(
os.path.join(self.tgt, "compose/metadata", f),
os.path.join(expected_dir, f),
)
@parameterized.expand(["empty-metadata", "basic-metadata"])
def test_merge_into_empty(self, fixture):
self.tgt = os.path.join(self.topdir, "target")
conf = o.Config(self.tgt, "production", None, None, None, None, [])
part = o.ComposePart("test", "/tmp/my.conf")
part.path = os.path.join(FIXTURE_DIR, "DP-1.0-20181001.n.0")
shutil.copytree(os.path.join(FIXTURE_DIR, fixture), self.tgt)
o.update_metadata(conf, part)
self.assertEqualMetadata(fixture + "-merged")
class TestCopyPart(PungiTestCase):
@mock.patch("pungi_utils.orchestrator.hardlink_dir")
def test_copy(self, hd):
self.tgt = os.path.join(self.topdir, "target")
conf = o.Config(self.tgt, "production", None, None, None, None, [])
linker = mock.Mock()
part = o.ComposePart("test", "/tmp/my.conf")
part.path = os.path.join(FIXTURE_DIR, "DP-1.0-20161013.t.4")
o.copy_part(conf, linker, part)
six.assertCountEqual(
self,
hd.call_args_list,
[
mock.call(
linker,
os.path.join(part.path, "compose", variant),
os.path.join(self.tgt, "compose", variant),
)
for variant in ["Client", "Server"]
],
)
class TestHardlinkDir(PungiTestCase):
def test_hardlinking(self):
linker = mock.Mock()
src = os.path.join(self.topdir, "src")
dst = os.path.join(self.topdir, "dst")
files = ["file.txt", "nested/deep/another.txt"]
for f in files:
touch(os.path.join(src, f))
o.hardlink_dir(linker, src, dst)
six.assertCountEqual(
self,
linker.queue_put.call_args_list,
[mock.call((os.path.join(src, f), os.path.join(dst, f))) for f in files],
)
class TestCheckFinishedProcesses(BaseTestCase):
def test_nothing_finished(self):
k1 = mock.Mock(returncode=None)
v1 = mock.Mock()
processes = {k1: v1}
six.assertCountEqual(self, o.check_finished_processes(processes), [])
def test_yields_finished(self):
k1 = mock.Mock(returncode=None)
v1 = mock.Mock()
k2 = mock.Mock(returncode=0)
v2 = mock.Mock()
processes = {k1: v1, k2: v2}
six.assertCountEqual(self, o.check_finished_processes(processes), [(k2, v2)])
def test_yields_failed(self):
k1 = mock.Mock(returncode=1)
v1 = mock.Mock()
processes = {k1: v1}
six.assertCountEqual(self, o.check_finished_processes(processes), [(k1, v1)])
class _Part(object):
def __init__(self, name, parent=None, fails=False, status=None):
self.name = name
self.finished = False
self.status = o.Status.WAITING if parent else o.Status.READY
if status:
self.status = status
self.proc = mock.Mock(name="proc_%s" % name, pid=hash(self))
self.parent = parent
self.fails = fails
self.failable = False
self.path = "/path/to/%s" % name
self.blocked_on = set([parent]) if parent else set()
def is_finished(self):
return self.finished or self.status == "FINISHED"
def __repr__(self):
return "<_Part(%r, parent=%r)>" % (self.name, self.parent)
def with_mocks(parts, finish_order, wait_results):
"""Setup all mocks and create dict with the parts.
:param finish_order: nested list: first element contains parts that finish
in first iteration, etc.
:param wait_results: list of names of processes that are returned by wait in each
iteration
"""
def decorator(func):
@wraps(func)
def worker(self, lp, update_status, cfp, hf, sp, wait):
self.parts = dict((p.name, p) for p in parts)
self.linker = lp.return_value.__enter__.return_value
update_status.side_effect = self.mock_update
hf.side_effect = self.mock_finish
sp.side_effect = self.mock_start
finish = [[]]
for grp in finish_order:
finish.append([(self.parts[p].proc, self.parts[p]) for p in grp])
cfp.side_effect = finish
wait.side_effect = [(self.parts[p].proc.pid, 0) for p in wait_results]
func(self)
self.assertEqual(lp.call_args_list, [mock.call("hardlink")])
return worker
return decorator
@mock.patch("os.wait")
@mock.patch("pungi_utils.orchestrator.start_part")
@mock.patch("pungi_utils.orchestrator.handle_finished")
@mock.patch("pungi_utils.orchestrator.check_finished_processes")
@mock.patch("pungi_utils.orchestrator.update_status")
@mock.patch("pungi_utils.orchestrator.linker_pool")
class TestRunAll(BaseTestCase):
def setUp(self):
self.maxDiff = None
self.conf = mock.Mock(name="global_config")
self.calls = []
def mock_update(self, global_config, parts):
self.assertEqual(global_config, self.conf)
self.assertEqual(parts, self.parts)
self.calls.append("update_status")
def mock_start(self, global_config, parts, part):
self.assertEqual(global_config, self.conf)
self.assertEqual(parts, self.parts)
self.calls.append(("start_part", part.name))
part.status = o.Status.STARTED
return part.proc
@property
def sorted_calls(self):
"""Sort the consecutive calls of the same function based on the argument."""
def key(val):
return val[0] if isinstance(val, tuple) else val
return list(
itertools.chain.from_iterable(
sorted(grp, key=operator.itemgetter(1))
for _, grp in itertools.groupby(self.calls, key)
)
)
def mock_finish(self, global_config, linker, parts, proc, part):
self.assertEqual(global_config, self.conf)
self.assertEqual(linker, self.linker)
self.assertEqual(parts, self.parts)
self.calls.append(("handle_finished", part.name))
for child in parts.values():
if child.parent == part.name:
child.status = o.Status.BLOCKED if part.fails else o.Status.READY
part.status = "DOOMED" if part.fails else "FINISHED"
@with_mocks(
[_Part("fst"), _Part("snd", parent="fst")], [["fst"], ["snd"]], ["fst", "snd"]
)
def test_sequential(self):
o.run_all(self.conf, self.parts)
self.assertEqual(
self.sorted_calls,
[
# First iteration starts fst
"update_status",
("start_part", "fst"),
# Second iteration handles finish of fst and starts snd
"update_status",
("handle_finished", "fst"),
("start_part", "snd"),
# Third iteration handles finish of snd
"update_status",
("handle_finished", "snd"),
# Final update of status
"update_status",
],
)
@with_mocks([_Part("fst"), _Part("snd")], [["fst", "snd"]], ["fst"])
def test_parallel(self):
o.run_all(self.conf, self.parts)
self.assertEqual(
self.sorted_calls,
[
# First iteration starts both fst and snd
"update_status",
("start_part", "fst"),
("start_part", "snd"),
# Second iteration handles finish of both of them
"update_status",
("handle_finished", "fst"),
("handle_finished", "snd"),
# Final update of status
"update_status",
],
)
@with_mocks(
[_Part("1"), _Part("2", parent="1"), _Part("3", parent="1")],
[["1"], ["2", "3"]],
["1", "2"],
)
def test_waits_for_dep_then_parallel_with_simultaneous_end(self):
o.run_all(self.conf, self.parts)
self.assertEqual(
self.sorted_calls,
[
# First iteration starts first part
"update_status",
("start_part", "1"),
# Second iteration starts 2 and 3
"update_status",
("handle_finished", "1"),
("start_part", "2"),
("start_part", "3"),
# Both 2 and 3 end in third iteration
"update_status",
("handle_finished", "2"),
("handle_finished", "3"),
# Final update of status
"update_status",
],
)
@with_mocks(
[_Part("1"), _Part("2", parent="1"), _Part("3", parent="1")],
[["1"], ["3"], ["2"]],
["1", "3", "2"],
)
def test_waits_for_dep_then_parallel_with_different_end_times(self):
o.run_all(self.conf, self.parts)
self.assertEqual(
self.sorted_calls,
[
# First iteration starts first part
"update_status",
("start_part", "1"),
# Second iteration starts 2 and 3
"update_status",
("handle_finished", "1"),
("start_part", "2"),
("start_part", "3"),
# Third iteration sees 3 finish
"update_status",
("handle_finished", "3"),
# Fourth iteration, 2 finishes
"update_status",
("handle_finished", "2"),
# Final update of status
"update_status",
],
)
@with_mocks(
[_Part("fst", fails=True), _Part("snd", parent="fst")], [["fst"]], ["fst"]
)
def test_blocked(self):
o.run_all(self.conf, self.parts)
self.assertEqual(
self.sorted_calls,
[
# First iteration starts first part
"update_status",
("start_part", "fst"),
# Second iteration handles fail of first part
"update_status",
("handle_finished", "fst"),
# Final update of status
"update_status",
],
)
@mock.patch("pungi_utils.orchestrator.get_compose_dir")
class TestGetTargetDir(BaseTestCase):
def test_with_absolute_path(self, gcd):
config = {"target": "/tgt", "compose_type": "nightly"}
cfg = mock.Mock()
cfg.get.side_effect = lambda _, k: config[k]
ci = mock.Mock()
res = o.get_target_dir(cfg, ci, None, reldir="/checkout")
self.assertEqual(res, gcd.return_value)
self.assertEqual(
gcd.call_args_list,
[mock.call("/tgt", ci, compose_type="nightly", compose_label=None)],
)
def test_with_relative_path(self, gcd):
config = {"target": "tgt", "compose_type": "nightly"}
cfg = mock.Mock()
cfg.get.side_effect = lambda _, k: config[k]
ci = mock.Mock()
res = o.get_target_dir(cfg, ci, None, reldir="/checkout")
self.assertEqual(res, gcd.return_value)
self.assertEqual(
gcd.call_args_list,
[
mock.call(
"/checkout/tgt", ci, compose_type="nightly", compose_label=None
)
],
)
class TestComputeStatus(BaseTestCase):
@parameterized.expand(
[
([("FINISHED", False)], "FINISHED"),
([("FINISHED", False), ("STARTED", False)], "STARTED"),
([("FINISHED", False), ("STARTED", False), ("WAITING", False)], "STARTED"),
([("FINISHED", False), ("DOOMED", False)], "DOOMED"),
(
[("FINISHED", False), ("BLOCKED", True), ("DOOMED", True)],
"FINISHED_INCOMPLETE",
),
([("FINISHED", False), ("BLOCKED", False), ("DOOMED", True)], "DOOMED"),
([("FINISHED", False), ("DOOMED", True)], "FINISHED_INCOMPLETE"),
([("FINISHED", False), ("STARTED", False), ("DOOMED", False)], "STARTED"),
]
)
def test_cases(self, statuses, expected):
self.assertEqual(o.compute_status(statuses), expected)
class TestUpdateStatus(PungiTestCase):
def test_updating(self):
os.makedirs(os.path.join(self.topdir, "compose/metadata"))
conf = o.Config(
self.topdir, "production", "RC-1.0", "/old", "/cfg", 1234, ["--quiet"]
)
o.update_status(
conf,
{"1": _Part("1", status="FINISHED"), "2": _Part("2", status="STARTED")},
)
self.assertFileContent(os.path.join(self.topdir, "STATUS"), "STARTED")
self.assertFileContent(
os.path.join(self.topdir, "compose/metadata/parts.json"),
dedent(
"""\
{
"1": {
"path": "/path/to/1",
"status": "FINISHED"
},
"2": {
"path": "/path/to/2",
"status": "STARTED"
}
}
"""
),
)
@mock.patch("pungi_utils.orchestrator.get_target_dir")
class TestPrepareComposeDir(PungiTestCase):
def setUp(self):
super(TestPrepareComposeDir, self).setUp()
self.conf = mock.Mock(name="config")
self.main_config = "/some/config"
self.compose_info = mock.Mock(name="compose_info")
def test_new_compose(self, gtd):
def mock_get_target(conf, compose_info, label, reldir):
self.assertEqual(conf, self.conf)
self.assertEqual(compose_info, self.compose_info)
self.assertEqual(label, args.label)
self.assertEqual(reldir, "/some")
touch(os.path.join(self.topdir, "work/global/composeinfo-base.json"), "WOO")
return self.topdir
gtd.side_effect = mock_get_target
args = mock.Mock(name="args", spec=["label"])
retval = o.prepare_compose_dir(
self.conf, args, self.main_config, self.compose_info
)
self.assertEqual(retval, self.topdir)
self.assertFileContent(
os.path.join(self.topdir, "compose/metadata/composeinfo.json"), "WOO"
)
self.assertTrue(os.path.isdir(os.path.join(self.topdir, "logs")))
self.assertTrue(os.path.isdir(os.path.join(self.topdir, "parts")))
self.assertTrue(os.path.isdir(os.path.join(self.topdir, "work/global")))
self.assertFileContent(os.path.join(self.topdir, "STATUS"), "STARTED")
def test_restarting_compose(self, gtd):
args = mock.Mock(name="args", spec=["label", "compose_path"])
retval = o.prepare_compose_dir(
self.conf, args, self.main_config, self.compose_info
)
self.assertEqual(gtd.call_args_list, [])
self.assertEqual(retval, args.compose_path)
class TestLoadPartsMetadata(PungiTestCase):
def test_loading(self):
touch(
os.path.join(self.topdir, "compose/metadata/parts.json"), '{"foo": "bar"}'
)
conf = mock.Mock(target=self.topdir)
self.assertEqual(o.load_parts_metadata(conf), {"foo": "bar"})
@mock.patch("pungi_utils.orchestrator.load_parts_metadata")
class TestSetupForRestart(BaseTestCase):
def setUp(self):
self.conf = mock.Mock(name="global_config")
def test_restart_ok(self, lpm):
lpm.return_value = {
"p1": {"status": "FINISHED", "path": "/p1"},
"p2": {"status": "DOOMED", "path": "/p2"},
}
parts = {"p1": _Part("p1"), "p2": _Part("p2", parent="p1")}
o.setup_for_restart(self.conf, parts, ["p2"])
self.assertEqual(parts["p1"].status, "FINISHED")
self.assertEqual(parts["p1"].path, "/p1")
self.assertEqual(parts["p2"].status, "READY")
self.assertEqual(parts["p2"].path, None)
def test_restart_one_blocked_one_ok(self, lpm):
lpm.return_value = {
"p1": {"status": "DOOMED", "path": "/p1"},
"p2": {"status": "DOOMED", "path": "/p2"},
"p3": {"status": "WAITING", "path": None},
}
parts = {
"p1": _Part("p1"),
"p2": _Part("p2", parent="p1"),
"p3": _Part("p3", parent="p2"),
}
o.setup_for_restart(self.conf, parts, ["p1", "p3"])
self.assertEqual(parts["p1"].status, "READY")
self.assertEqual(parts["p1"].path, None)
self.assertEqual(parts["p2"].status, "DOOMED")
self.assertEqual(parts["p2"].path, "/p2")
self.assertEqual(parts["p3"].status, "WAITING")
self.assertEqual(parts["p3"].path, None)
def test_restart_all_blocked(self, lpm):
lpm.return_value = {
"p1": {"status": "DOOMED", "path": "/p1"},
"p2": {"status": "STARTED", "path": "/p2"},
}
parts = {"p1": _Part("p1"), "p2": _Part("p2", parent="p1")}
with self.assertRaises(RuntimeError):
o.setup_for_restart(self.conf, parts, ["p2"])
self.assertEqual(parts["p1"].status, "DOOMED")
self.assertEqual(parts["p1"].path, "/p1")
self.assertEqual(parts["p2"].status, "WAITING")
self.assertEqual(parts["p2"].path, None)
@mock.patch("atexit.register")
@mock.patch("kobo.shortcuts.run")
class TestRunKinit(BaseTestCase):
def test_without_config(self, run, register):
conf = mock.Mock()
conf.getboolean.return_value = False
o.run_kinit(conf)
self.assertEqual(run.call_args_list, [])
self.assertEqual(register.call_args_list, [])
@mock.patch.dict("os.environ")
def test_with_config(self, run, register):
conf = mock.Mock()
conf.getboolean.return_value = True
conf.get.side_effect = lambda section, option: option
o.run_kinit(conf)
self.assertEqual(
run.call_args_list,
[mock.call(["kinit", "-k", "-t", "kerberos_keytab", "kerberos_principal"])],
)
self.assertEqual(
register.call_args_list, [mock.call(os.remove, os.environ["KRB5CCNAME"])]
)
@mock.patch.dict("os.environ", {}, clear=True)
class TestGetScriptEnv(BaseTestCase):
def test_without_metadata(self):
env = o.get_script_env("/foobar")
self.assertEqual(env, {"COMPOSE_PATH": "/foobar"})
def test_with_metadata(self):
compose_dir = os.path.join(FIXTURE_DIR, "DP-1.0-20161013.t.4")
env = o.get_script_env(compose_dir)
self.maxDiff = None
self.assertEqual(
env,
{
"COMPOSE_PATH": compose_dir,
"COMPOSE_ID": "DP-1.0-20161013.t.4",
"COMPOSE_DATE": "20161013",
"COMPOSE_TYPE": "test",
"COMPOSE_RESPIN": "4",
"COMPOSE_LABEL": "",
"RELEASE_ID": "DP-1.0",
"RELEASE_NAME": "Dummy Product",
"RELEASE_SHORT": "DP",
"RELEASE_VERSION": "1.0",
"RELEASE_TYPE": "ga",
"RELEASE_IS_LAYERED": "",
},
)
class TestRunScripts(BaseTestCase):
@mock.patch("pungi_utils.orchestrator.get_script_env")
@mock.patch("kobo.shortcuts.run")
def test_run_scripts(self, run, get_env):
commands = """
date
env
"""
o.run_scripts("pref_", "/tmp/compose", commands)
self.assertEqual(
run.call_args_list,
[
mock.call(
"date",
logfile="/tmp/compose/logs/pref_0.log",
env=get_env.return_value,
),
mock.call(
"env",
logfile="/tmp/compose/logs/pref_1.log",
env=get_env.return_value,
),
],
)
@mock.patch("pungi.notifier.PungiNotifier")
class TestSendNotification(BaseTestCase):
def test_no_command(self, notif):
o.send_notification("/foobar", None, None)
self.assertEqual(notif.mock_calls, [])
@mock.patch("pungi.util.load_config")
def test_with_command_and_translate(self, load_config, notif):
compose_dir = os.path.join(FIXTURE_DIR, "DP-1.0-20161013.t.4")
load_config.return_value = {
"translate_paths": [(os.path.dirname(compose_dir), "http://example.com")],
}
parts = {"foo": mock.Mock()}
o.send_notification(compose_dir, "handler", parts)
self.assertEqual(len(notif.mock_calls), 2)
self.assertEqual(notif.mock_calls[0], mock.call(["handler"]))
_, args, kwargs = notif.mock_calls[1]
self.assertEqual(args, ("status-change",))
self.assertEqual(
kwargs,
{
"status": "FINISHED",
"workdir": compose_dir,
"location": "http://example.com/DP-1.0-20161013.t.4",
"compose_id": "DP-1.0-20161013.t.4",
"compose_date": "20161013",
"compose_type": "test",
"compose_respin": "4",
"compose_label": None,
"release_id": "DP-1.0",
"release_name": "Dummy Product",
"release_short": "DP",
"release_version": "1.0",
"release_type": "ga",
"release_is_layered": False,
},
)
self.assertEqual(load_config.call_args_list, [mock.call(parts["foo"].config)])

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
import mock
from unittest import mock
import json
import copy
@ -171,6 +171,7 @@ class OSBSThreadTest(helpers.PungiTestCase):
self.topdir,
{
"koji_profile": "koji",
"koji_cache": "/tmp",
"translate_paths": [(self.topdir, "http://root")],
},
)

View File

@ -1,16 +1,78 @@
# -*- coding: utf-8 -*-
import mock
from unittest import mock
import os
import shutil
import tempfile
import unittest
import koji as orig_koji
from tests import helpers
from pungi import compose
from pungi.phases import osbuild
from pungi.checks import validate
class OSBuildPhaseHelperFuncsTest(unittest.TestCase):
@mock.patch("pungi.compose.ComposeInfo")
def setUp(self, ci):
self.tmp_dir = tempfile.mkdtemp()
conf = {"translate_paths": [(self.tmp_dir, "http://example.com")]}
ci.return_value.compose.respin = 0
ci.return_value.compose.id = "RHEL-8.0-20180101.n.0"
ci.return_value.compose.date = "20160101"
ci.return_value.compose.type = "nightly"
ci.return_value.compose.type_suffix = ".n"
ci.return_value.compose.label = "RC-1.0"
ci.return_value.compose.label_major_version = "1"
compose_dir = os.path.join(self.tmp_dir, ci.return_value.compose.id)
self.compose = compose.Compose(conf, compose_dir)
server_variant = mock.Mock(uid="Server", type="variant")
client_variant = mock.Mock(uid="Client", type="variant")
self.compose.all_variants = {
"Server": server_variant,
"Client": client_variant,
}
def tearDown(self):
shutil.rmtree(self.tmp_dir)
def test__get_repo_urls(self):
repos = [
"http://example.com/repo",
"Server",
{
"baseurl": "Client",
"package_sets": ["build"],
},
{
"baseurl": "ftp://example.com/linux/repo",
"package_sets": ["build"],
},
]
expect = [
"http://example.com/repo",
"http://example.com/RHEL-8.0-20180101.n.0/compose/Server/$basearch/os",
{
"baseurl": "http://example.com/RHEL-8.0-20180101.n.0/compose/Client/"
+ "$basearch/os",
"package_sets": ["build"],
},
{
"baseurl": "ftp://example.com/linux/repo",
"package_sets": ["build"],
},
]
self.assertEqual(
osbuild.OSBuildPhase._get_repo_urls(self.compose, repos), expect
)
class OSBuildPhaseTest(helpers.PungiTestCase):
@mock.patch("pungi.phases.osbuild.ThreadPool")
def test_run(self, ThreadPool):
@ -124,6 +186,49 @@ class OSBuildPhaseTest(helpers.PungiTestCase):
)
self.assertNotEqual(validate(compose.conf), ([], []))
@mock.patch("pungi.phases.osbuild.ThreadPool")
def test_rich_repos(self, ThreadPool):
repo = {"baseurl": "http://example.com/repo", "package_sets": ["build"]}
cfg = {
"name": "test-image",
"distro": "rhel-8",
"version": "1",
"target": "image-target",
"arches": ["x86_64"],
"image_types": ["qcow2"],
"repo": [repo],
}
compose = helpers.DummyCompose(
self.topdir, {"osbuild": {"^Everything$": [cfg]}}
)
self.assertValidConfig(compose.conf)
pool = ThreadPool.return_value
phase = osbuild.OSBuildPhase(compose)
phase.run()
self.assertEqual(len(pool.add.call_args_list), 1)
self.assertEqual(
pool.queue_put.call_args_list,
[
mock.call(
(
compose,
compose.variants["Everything"],
cfg,
["x86_64"],
"1",
None,
"image-target",
[repo, self.topdir + "/compose/Everything/$arch/os"],
[],
),
),
],
)
class RunOSBuildThreadTest(helpers.PungiTestCase):
def setUp(self):
@ -134,6 +239,7 @@ class RunOSBuildThreadTest(helpers.PungiTestCase):
self.topdir,
{
"koji_profile": "koji",
"koji_cache": "/tmp",
"translate_paths": [(self.topdir, "http://root")],
},
)
@ -189,7 +295,13 @@ class RunOSBuildThreadTest(helpers.PungiTestCase):
"1", # version
"15", # release
"image-target",
[self.topdir + "/compose/Everything/$arch/os"],
[
self.topdir + "/compose/Everything/$arch/os",
{
"baseurl": self.topdir + "/compose/Everything/$arch/os",
"package_sets": ["build"],
},
],
["x86_64"],
),
1,
@ -211,7 +323,13 @@ class RunOSBuildThreadTest(helpers.PungiTestCase):
["aarch64", "x86_64"],
opts={
"release": "15",
"repo": [self.topdir + "/compose/Everything/$arch/os"],
"repo": [
self.topdir + "/compose/Everything/$arch/os",
{
"baseurl": self.topdir + "/compose/Everything/$arch/os",
"package_sets": ["build"],
},
],
},
),
mock.call.save_task_id(1234),

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
import mock
from unittest import mock
import os
@ -103,6 +103,7 @@ class OstreeThreadTest(helpers.PungiTestCase):
"release_name": "Fedora",
"release_version": "Rawhide",
"koji_profile": "koji",
"koji_cache": "/tmp",
"runroot_tag": "rrt",
"image_volid_formats": ["{release_short}-{variant}-{arch}"],
"translate_paths": [(self.topdir + "/work", "http://example.com/work")],

View File

@ -2,7 +2,7 @@
import json
import mock
from unittest import mock
import os
@ -123,6 +123,7 @@ class OSTreeThreadTest(helpers.PungiTestCase):
self.topdir,
{
"koji_profile": "koji",
"koji_cache": "/tmp",
"runroot_tag": "rrt",
"translate_paths": [(self.topdir, "http://example.com")],
},

View File

@ -4,7 +4,7 @@
import json
import os
import mock
from unittest import mock
import six
import yaml
@ -315,7 +315,6 @@ class OstreeTreeScriptTest(helpers.PungiTestCase):
@mock.patch("kobo.shortcuts.run")
def test_extra_config_with_keep_original_sources(self, run):
configdir = os.path.join(self.topdir, "config")
self._make_dummy_config_dir(configdir)
treefile = os.path.join(configdir, "fedora-atomic-docker-host.json")

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
import mock
from unittest import mock
import os

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
import mock
from unittest import mock
import os
try:

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
import mock
from unittest import mock
try:
import unittest2 as unittest

View File

@ -2,7 +2,7 @@
import os
import mock
from unittest import mock
import six
from pungi.module_util import Modulemd
@ -47,7 +47,7 @@ class TestMaterializedPkgsetCreate(helpers.PungiTestCase):
pkgset.name = name
pkgset.reuse = None
def mock_subset(primary, arch_list, exclusive_noarch):
def mock_subset(primary, arch_list, **kwargs):
self.subsets[primary] = mock.Mock()
return self.subsets[primary]
@ -73,10 +73,16 @@ class TestMaterializedPkgsetCreate(helpers.PungiTestCase):
self.assertEqual(result["amd64"], self.subsets["amd64"])
self.pkgset.subset.assert_any_call(
"x86_64", ["x86_64", "noarch", "src"], exclusive_noarch=True
"x86_64",
["x86_64", "noarch", "src"],
exclusive_noarch=True,
inherit_to_noarch=True,
)
self.pkgset.subset.assert_any_call(
"amd64", ["amd64", "x86_64", "noarch", "src"], exclusive_noarch=True
"amd64",
["amd64", "x86_64", "noarch", "src"],
exclusive_noarch=True,
inherit_to_noarch=True,
)
for arch, pkgset in result.package_sets.items():

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
import ddt
import mock
from unittest import mock
import os
import six
@ -133,19 +133,43 @@ class PkgsetCompareMixin(object):
self.assertEqual({}, actual)
class DummySystem(object):
def __init__(self):
self.methods = ["_listapi", "Dummy", "getRPM", "getRPMChecksums"]
def listMethods(self):
return self.methods
@ddt.ddt
@mock.patch("pungi.phases.pkgset.pkgsets.ReaderPool", new=FakePool)
@mock.patch("kobo.pkgset.FileCache", new=MockFileCache)
class TestKojiPkgset(PkgsetCompareMixin, helpers.PungiTestCase):
@classmethod
def setUpClass(cls) -> None:
cls.patcher = mock.patch.object(
pkgsets.KojiMockPackageSet,
'_is_rpm_signed',
return_value=True,
)
cls.patcher.start()
@classmethod
def tearDownClass(cls) -> None:
cls.patcher.stop()
def setUp(self):
super(TestKojiPkgset, self).setUp()
with open(os.path.join(helpers.FIXTURE_DIR, "tagged-rpms.json")) as f:
self.tagged_rpms = json.load(f)
self.path_info = MockPathInfo(self.topdir)
self.koji_downloader = helpers.FSKojiDownloader()
self.koji_wrapper = mock.Mock()
self.koji_wrapper.koji_proxy.listTaggedRPMS.return_value = self.tagged_rpms
self.koji_wrapper.koji_proxy.system = DummySystem()
self.koji_wrapper.koji_module.pathinfo = self.path_info
def _touch_files(self, filenames):
@ -176,7 +200,9 @@ class TestKojiPkgset(PkgsetCompareMixin, helpers.PungiTestCase):
]
)
pkgset = package_set("pkgset", self.koji_wrapper, [None])
pkgset = package_set(
"pkgset", self.koji_wrapper, [None], downloader=self.koji_downloader
)
result = pkgset.populate("f25")
@ -214,7 +240,11 @@ class TestKojiPkgset(PkgsetCompareMixin, helpers.PungiTestCase):
)
pkgset = package_set(
"pkgset", self.koji_wrapper, [None], arches=["x86_64"]
"pkgset",
self.koji_wrapper,
[None],
arches=["x86_64"],
downloader=self.koji_downloader,
)
result = pkgset.populate("f25")
@ -244,7 +274,11 @@ class TestKojiPkgset(PkgsetCompareMixin, helpers.PungiTestCase):
)
pkgset = pkgsets.KojiPackageSet(
"pkgset", self.koji_wrapper, ["cafebabe", "deadbeef"], arches=["x86_64"]
"pkgset",
self.koji_wrapper,
["cafebabe", "deadbeef"],
arches=["x86_64"],
downloader=self.koji_downloader,
)
result = pkgset.populate("f25")
@ -273,7 +307,11 @@ class TestKojiPkgset(PkgsetCompareMixin, helpers.PungiTestCase):
)
pkgset = pkgsets.KojiPackageSet(
"pkgset", self.koji_wrapper, ["cafebabe", None], arches=["x86_64"]
"pkgset",
self.koji_wrapper,
["cafebabe", None],
arches=["x86_64"],
downloader=self.koji_downloader,
)
result = pkgset.populate("f25")
@ -295,7 +333,11 @@ class TestKojiPkgset(PkgsetCompareMixin, helpers.PungiTestCase):
def test_can_not_find_signed_package(self):
pkgset = pkgsets.KojiPackageSet(
"pkgset", self.koji_wrapper, ["cafebabe"], arches=["x86_64"]
"pkgset",
self.koji_wrapper,
["cafebabe"],
arches=["x86_64"],
downloader=self.koji_downloader,
)
with self.assertRaises(RuntimeError) as ctx:
@ -307,7 +349,7 @@ class TestKojiPkgset(PkgsetCompareMixin, helpers.PungiTestCase):
)
figure = re.compile(
r"^RPM\(s\) not found for sigs: .+Check log for details.+bash-4\.3\.42-4\.fc24.+bash-debuginfo-4\.3\.42-4\.fc24$", # noqa: E501
r"^RPM\(s\) not found for sigs: .+Check log for details.+bash-4\.3\.42-4\.fc24\.x86_64.+bash-debuginfo-4\.3\.42-4\.fc24\.x86_64$", # noqa: E501
re.DOTALL,
)
self.assertRegex(str(ctx.exception), figure)
@ -334,6 +376,7 @@ class TestKojiPkgset(PkgsetCompareMixin, helpers.PungiTestCase):
arches=["x86_64"],
signed_packages_retries=2,
signed_packages_wait=5,
downloader=self.koji_downloader,
)
result = pkgset.populate("f25")
@ -371,6 +414,7 @@ class TestKojiPkgset(PkgsetCompareMixin, helpers.PungiTestCase):
["cafebabe"],
arches=["x86_64"],
allow_invalid_sigkeys=True,
downloader=self.koji_downloader,
)
pkgset.populate("f25")
@ -384,14 +428,18 @@ class TestKojiPkgset(PkgsetCompareMixin, helpers.PungiTestCase):
pkgset.raise_invalid_sigkeys_exception(pkgset.invalid_sigkey_rpms)
figure = re.compile(
r"^RPM\(s\) not found for sigs: .+Check log for details.+bash-4\.3\.42-4\.fc24.+bash-debuginfo-4\.3\.42-4\.fc24$", # noqa: E501
r"^RPM\(s\) not found for sigs: .+Check log for details.+bash-4\.3\.42-4\.fc24\.x86_64.+bash-debuginfo-4\.3\.42-4\.fc24\.x86_64$", # noqa: E501
re.DOTALL,
)
self.assertRegex(str(ctx.exception), figure)
def test_can_not_find_any_package(self):
pkgset = pkgsets.KojiPackageSet(
"pkgset", self.koji_wrapper, ["cafebabe", None], arches=["x86_64"]
"pkgset",
self.koji_wrapper,
["cafebabe", None],
arches=["x86_64"],
downloader=self.koji_downloader,
)
with self.assertRaises(RuntimeError) as ctx:
@ -416,6 +464,7 @@ class TestKojiPkgset(PkgsetCompareMixin, helpers.PungiTestCase):
arches=["x86_64"],
signed_packages_retries=2,
signed_packages_wait=5,
downloader=self.koji_downloader,
)
with self.assertRaises(RuntimeError) as ctx:
@ -456,6 +505,7 @@ class TestKojiPkgset(PkgsetCompareMixin, helpers.PungiTestCase):
[None],
packages=["bash"],
populate_only_packages=True,
downloader=self.koji_downloader,
)
result = pkgset.populate("f25")
@ -564,6 +614,7 @@ class TestKojiPkgset(PkgsetCompareMixin, helpers.PungiTestCase):
[None],
arches=["x86_64"],
cache_region=cache_region,
downloader=self.koji_downloader,
)
# Try calling the populate twice, but expect just single listTaggedRPMs
@ -607,6 +658,7 @@ class TestKojiPkgset(PkgsetCompareMixin, helpers.PungiTestCase):
[None],
arches=["x86_64"],
cache_region=cache_region,
downloader=self.koji_downloader,
)
# Try calling the populate twice with different event id. It must not
@ -664,7 +716,11 @@ class TestKojiPkgset(PkgsetCompareMixin, helpers.PungiTestCase):
]
pkgset = package_set(
"pkgset", self.koji_wrapper, [None], extra_builds=["pungi-4.1.3-3.fc25"]
"pkgset",
self.koji_wrapper,
[None],
extra_builds=["pungi-4.1.3-3.fc25"],
downloader=self.koji_downloader,
)
result = pkgset.populate("f25")
@ -876,6 +932,8 @@ class TestReuseKojiPkgset(helpers.PungiTestCase):
"include_packages": None,
"rpms_by_arch": mock.Mock(),
"srpms_by_name": mock.Mock(),
"inherit_to_noarch": True,
"exclusive_noarch": True,
}
)
self.pkgset.old_file_cache = mock.Mock()
@ -1085,6 +1143,8 @@ class TestReuseKojiMockPkgset(helpers.PungiTestCase):
"include_packages": None,
"rpms_by_arch": mock.Mock(),
"srpms_by_name": mock.Mock(),
"exclusive_noarch": True,
"inherit_to_noarch": True,
}
)
self.pkgset.old_file_cache = mock.Mock()
@ -1166,6 +1226,28 @@ class TestMergePackageSets(PkgsetCompareMixin, unittest.TestCase):
first.rpms_by_arch, {"i686": ["rpms/bash@4.3.42@4.fc24@i686"], "noarch": []}
)
def test_merge_doesnt_exclude_noarch_exclude_arch_when_configured(self):
first = pkgsets.PackageSetBase("first", [None])
second = pkgsets.PackageSetBase("second", [None])
pkg = first.file_cache.add("rpms/bash@4.3.42@4.fc24@i686")
first.rpms_by_arch.setdefault(pkg.arch, []).append(pkg)
pkg = second.file_cache.add("rpms/pungi@4.1.3@3.fc25@noarch")
pkg.excludearch = ["i686"]
second.rpms_by_arch.setdefault(pkg.arch, []).append(pkg)
first.merge(second, "i386", ["i686", "noarch"], inherit_to_noarch=False)
print(first.rpms_by_arch)
self.assertPkgsetEqual(
first.rpms_by_arch,
{
"i686": ["rpms/bash@4.3.42@4.fc24@i686"],
"noarch": ["rpms/pungi@4.1.3@3.fc25@noarch"],
},
)
def test_merge_excludes_noarch_exclusive_arch(self):
first = pkgsets.PackageSetBase("first", [None])
second = pkgsets.PackageSetBase("second", [None])

View File

@ -79,7 +79,8 @@ class TestPopulateGlobalPkgset(helpers.PungiTestCase):
def setUp(self):
super(TestPopulateGlobalPkgset, self).setUp()
self.compose = helpers.DummyCompose(
self.topdir, {"pkgset_koji_tag": "f25", "sigkeys": ["foo", "bar"]}
self.topdir,
{"pkgset_koji_tag": "f25", "sigkeys": ["foo", "bar"], "koji_cache": "/tmp"},
)
self.koji_wrapper = mock.Mock()
self.pkgset_path = os.path.join(
@ -98,7 +99,7 @@ class TestPopulateGlobalPkgset(helpers.PungiTestCase):
orig_pkgset = KojiPackageSet.return_value
pkgsets = source_koji.populate_global_pkgset(
self.compose, self.koji_wrapper, "/prefix", 123456
self.compose, self.koji_wrapper, 123456
)
self.assertEqual(len(pkgsets), 1)
@ -117,7 +118,11 @@ class TestPopulateGlobalPkgset(helpers.PungiTestCase):
def test_populate_with_multiple_koji_tags(self, KojiPackageSet, materialize):
self.compose = helpers.DummyCompose(
self.topdir,
{"pkgset_koji_tag": ["f25", "f25-extra"], "sigkeys": ["foo", "bar"]},
{
"pkgset_koji_tag": ["f25", "f25-extra"],
"sigkeys": ["foo", "bar"],
"koji_cache": "/tmp",
},
)
materialize.side_effect = self.mock_materialize
@ -125,7 +130,7 @@ class TestPopulateGlobalPkgset(helpers.PungiTestCase):
KojiPackageSet.return_value.reuse = None
pkgsets = source_koji.populate_global_pkgset(
self.compose, self.koji_wrapper, "/prefix", 123456
self.compose, self.koji_wrapper, 123456
)
self.assertEqual(len(pkgsets), 2)
@ -160,6 +165,7 @@ class TestPopulateGlobalPkgset(helpers.PungiTestCase):
{
"gather_method": "nodeps",
"pkgset_koji_tag": "f25",
"koji_cache": "/tmp",
"sigkeys": ["foo", "bar"],
"additional_packages": [(".*", {"*": ["pkg", "foo.x86_64"]})],
},
@ -168,7 +174,7 @@ class TestPopulateGlobalPkgset(helpers.PungiTestCase):
materialize.side_effect = self.mock_materialize
pkgsets = source_koji.populate_global_pkgset(
self.compose, self.koji_wrapper, "/prefix", 123456
self.compose, self.koji_wrapper, 123456
)
self.assertEqual(len(pkgsets), 1)
six.assertCountEqual(self, pkgsets[0].packages, ["pkg", "foo"])
@ -177,7 +183,9 @@ class TestPopulateGlobalPkgset(helpers.PungiTestCase):
class TestGetPackageSetFromKoji(helpers.PungiTestCase):
def setUp(self):
super(TestGetPackageSetFromKoji, self).setUp()
self.compose = helpers.DummyCompose(self.topdir, {"pkgset_koji_tag": "f25"})
self.compose = helpers.DummyCompose(
self.topdir, {"pkgset_koji_tag": "f25", "koji_cache": "/tmp"}
)
self.compose.koji_event = None
self.koji_wrapper = mock.Mock()
self.koji_wrapper.koji_proxy.getLastEvent.return_value = EVENT_INFO
@ -185,9 +193,7 @@ class TestGetPackageSetFromKoji(helpers.PungiTestCase):
@mock.patch("pungi.phases.pkgset.sources.source_koji.populate_global_pkgset")
def test_get_package_sets(self, pgp):
pkgsets = source_koji.get_pkgset_from_koji(
self.compose, self.koji_wrapper, "/prefix"
)
pkgsets = source_koji.get_pkgset_from_koji(self.compose, self.koji_wrapper)
six.assertCountEqual(
self, self.koji_wrapper.koji_proxy.mock_calls, [mock.call.getLastEvent()]
@ -196,7 +202,7 @@ class TestGetPackageSetFromKoji(helpers.PungiTestCase):
self.assertEqual(
pgp.call_args_list,
[mock.call(self.compose, self.koji_wrapper, "/prefix", EVENT_INFO)],
[mock.call(self.compose, self.koji_wrapper, EVENT_INFO)],
)
def test_get_koji_modules(self):
@ -446,14 +452,16 @@ class TestSourceKoji(helpers.PungiTestCase):
@mock.patch("pungi.phases.pkgset.sources.source_koji.get_pkgset_from_koji")
@mock.patch("pungi.wrappers.kojiwrapper.KojiWrapper")
def test_run(self, KojiWrapper, gpfk):
compose = helpers.DummyCompose(self.topdir, {"koji_profile": "koji"})
compose = helpers.DummyCompose(
self.topdir, {"koji_profile": "koji", "koji_cache": "/prefix"}
)
KojiWrapper.return_value.koji_module.config.topdir = "/prefix"
phase = source_koji.PkgsetSourceKoji(compose)
pkgsets, path_prefix = phase()
self.assertEqual(pkgsets, gpfk.return_value)
self.assertEqual(path_prefix, "/prefix/")
self.assertEqual(path_prefix, "/prefix")
self.assertEqual(KojiWrapper.mock_calls, [mock.call(compose)])
@ -685,6 +693,7 @@ class TestAddModuleToVariant(helpers.PungiTestCase):
def setUp(self):
super(TestAddModuleToVariant, self).setUp()
self.koji = mock.Mock()
self.compose = helpers.DummyCompose(self.topdir, {})
self.koji.koji_module.pathinfo.typedir.return_value = MMDS_DIR
files = ["modulemd.x86_64.txt", "modulemd.armv7hl.txt", "modulemd.txt"]
self.koji.koji_proxy.listArchives.return_value = [
@ -707,7 +716,9 @@ class TestAddModuleToVariant(helpers.PungiTestCase):
def test_adding_module(self):
variant = mock.Mock(arches=["armhfp", "x86_64"], arch_mmds={}, modules=[])
source_koji._add_module_to_variant(self.koji, variant, self.buildinfo)
source_koji._add_module_to_variant(
self.koji, variant, self.buildinfo, compose=self.compose
)
mod1 = variant.arch_mmds["armhfp"]["module:master:20190318:abcdef"]
self.assertEqual(mod1.get_NSVCA(), "module:master:20190318:abcdef:armhfp")
@ -729,7 +740,9 @@ class TestAddModuleToVariant(helpers.PungiTestCase):
modules=[{"name": "m1:latest-20190101:cafe", "glob": False}],
)
source_koji._add_module_to_variant(self.koji, variant, self.buildinfo)
source_koji._add_module_to_variant(
self.koji, variant, self.buildinfo, compose=self.compose
)
mod1 = variant.arch_mmds["armhfp"]["module:master:20190318:abcdef"]
self.assertEqual(mod1.get_NSVCA(), "module:master:20190318:abcdef:armhfp")
@ -746,7 +759,11 @@ class TestAddModuleToVariant(helpers.PungiTestCase):
variant = mock.Mock(arches=["armhfp", "x86_64"], arch_mmds={}, modules=[])
source_koji._add_module_to_variant(
self.koji, variant, self.buildinfo, add_to_variant_modules=True
self.koji,
variant,
self.buildinfo,
compose=self.compose,
add_to_variant_modules=True,
)
mod1 = variant.arch_mmds["armhfp"]["module:master:20190318:abcdef"]
@ -772,7 +789,11 @@ class TestAddModuleToVariant(helpers.PungiTestCase):
)
source_koji._add_module_to_variant(
self.koji, variant, self.buildinfo, add_to_variant_modules=True
self.koji,
variant,
self.buildinfo,
compose=self.compose,
add_to_variant_modules=True,
)
mod1 = variant.arch_mmds["armhfp"]["module:master:20190318:abcdef"]
@ -818,6 +839,7 @@ class TestAddModuleToVariantForKojiMock(helpers.PungiTestCase):
super(TestAddModuleToVariantForKojiMock, self).setUp()
self.koji = mock.Mock()
self.koji.koji_module.pathinfo.typedir.return_value = "/koji"
self.compose = helpers.DummyCompose(self.topdir, {})
self.koji.koji_module.pathinfo.topdir = MMDS_DIR
files = [
"modulemd.x86_64.txt",
@ -876,7 +898,10 @@ class TestAddModuleToVariantForKojiMock(helpers.PungiTestCase):
modules=[{"name": "m1:latest-20190101:cafe", "glob": False}],
)
source_koji._add_module_to_variant(self.koji, variant, self.buildinfo)
source_koji._add_module_to_variant(
self.koji, variant, self.buildinfo, compose=self.compose
)
mod = variant.arch_mmds["x86_64"]["m1:latest:20190101:cafe"]
self.assertEqual(mod.get_NSVCA(), "m1:latest:20190101:cafe:x86_64")

View File

@ -6,7 +6,7 @@ try:
except ImportError:
import unittest
import mock
from unittest import mock
import six
import pungi.phases.repoclosure as repoclosure_phase

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
import mock
from unittest import mock
import os
from pungi.runroot import Runroot

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
import mock
from unittest import mock
try:
import unittest2 as unittest
@ -13,8 +13,10 @@ import random
import os
import six
from parameterized import parameterized
from pungi.wrappers import scm
from tests.helpers import touch
from tests.helpers import touch, GIT_WITH_CREDS
from kobo.shortcuts import run
@ -109,37 +111,49 @@ class FileSCMTestCase(SCMBaseTest):
self.assertIn("No directories matched", str(ctx.exception))
CREDENTIALS_CONFIG = {"credential_helper": "!ch"}
class GitSCMTestCase(SCMBaseTest):
def assertCalls(self, mock_run, url, branch, command=None):
def tearDown(self):
shutil.rmtree("/tmp/pungi-temp-git-repos-%s" % os.getpid())
super(GitSCMTestCase, self).tearDown()
def assertCalls(self, mock_run, url, branch, command=None, with_creds=False):
git = GIT_WITH_CREDS if with_creds else ["git"]
command = [command] if command else []
self.assertEqual(
[call[0][0] for call in mock_run.call_args_list],
[
["git", "init"],
["git", "fetch", "--depth=1", url, branch],
git + ["fetch", "--depth=1", url, branch],
["git", "checkout", "FETCH_HEAD"],
]
+ command,
)
@mock.patch("pungi.wrappers.scm.run")
def test_get_file(self, run):
@parameterized.expand([("without_creds", {}), ("with_creds", CREDENTIALS_CONFIG)])
def test_get_file(self, _name, config):
def process(cmd, workdir=None, **kwargs):
touch(os.path.join(workdir, "some_file.txt"))
touch(os.path.join(workdir, "other_file.txt"))
run.side_effect = process
with mock.patch("pungi.wrappers.scm.run") as run:
run.side_effect = process
retval = scm.get_file_from_scm(
{
"scm": "git",
"repo": "git://example.com/git/repo.git",
"file": "some_file.txt",
"options": config,
},
self.destdir,
)
retval = scm.get_file_from_scm(
{
"scm": "git",
"repo": "git://example.com/git/repo.git",
"file": "some_file.txt",
},
self.destdir,
)
self.assertStructure(retval, ["some_file.txt"])
self.assertCalls(run, "git://example.com/git/repo.git", "master")
self.assertCalls(
run, "git://example.com/git/repo.git", "master", with_creds=bool(config)
)
@mock.patch("pungi.wrappers.scm.run")
def test_get_file_function(self, run):
@ -163,9 +177,10 @@ class GitSCMTestCase(SCMBaseTest):
self.assertEqual(retval, destination)
self.assertCalls(run, "git://example.com/git/repo.git", "master")
@mock.patch("pungi.wrappers.scm.run")
def test_get_file_fetch_fails(self, run):
@parameterized.expand([("without_creds", {}), ("with_creds", CREDENTIALS_CONFIG)])
def test_get_file_fetch_fails(self, _name, config):
url = "git://example.com/git/repo.git"
git = GIT_WITH_CREDS if config else ["git"]
def process(cmd, workdir=None, **kwargs):
if "fetch" in cmd:
@ -175,18 +190,20 @@ class GitSCMTestCase(SCMBaseTest):
touch(os.path.join(workdir, "some_file.txt"))
touch(os.path.join(workdir, "other_file.txt"))
run.side_effect = process
with mock.patch("pungi.wrappers.scm.run") as run:
run.side_effect = process
retval = scm.get_file_from_scm(
{"scm": "git", "repo": url, "file": "some_file.txt", "options": config},
self.destdir,
)
retval = scm.get_file_from_scm(
{"scm": "git", "repo": url, "file": "some_file.txt"}, self.destdir
)
self.assertStructure(retval, ["some_file.txt"])
self.assertEqual(
[call[0][0] for call in run.call_args_list],
[
["git", "init"],
[
"git",
git
+ [
"fetch",
"--depth=1",
"git://example.com/git/repo.git",
@ -194,7 +211,7 @@ class GitSCMTestCase(SCMBaseTest):
],
["git", "init"],
["git", "remote", "add", "origin", url],
["git", "remote", "update", "origin"],
git + ["remote", "update", "origin"],
["git", "checkout", "master"],
],
)
@ -243,20 +260,28 @@ class GitSCMTestCase(SCMBaseTest):
self.assertEqual(str(ctx.exception), "'make' failed with exit code 1")
@mock.patch("pungi.wrappers.scm.run")
def test_get_dir(self, run):
@parameterized.expand([("without_creds", {}), ("with_creds", CREDENTIALS_CONFIG)])
def test_get_dir(self, _name, config):
def process(cmd, workdir=None, **kwargs):
touch(os.path.join(workdir, "subdir", "first"))
touch(os.path.join(workdir, "subdir", "second"))
run.side_effect = process
with mock.patch("pungi.wrappers.scm.run") as run:
run.side_effect = process
retval = scm.get_dir_from_scm(
{
"scm": "git",
"repo": "git://example.com/git/repo.git",
"dir": "subdir",
"options": config,
},
self.destdir,
)
retval = scm.get_dir_from_scm(
{"scm": "git", "repo": "git://example.com/git/repo.git", "dir": "subdir"},
self.destdir,
)
self.assertStructure(retval, ["first", "second"])
self.assertCalls(run, "git://example.com/git/repo.git", "master")
self.assertCalls(
run, "git://example.com/git/repo.git", "master", with_creds=bool(config)
)
@mock.patch("pungi.wrappers.scm.run")
def test_get_dir_and_generate(self, run):

Some files were not shown because too many files have changed in this diff Show More