chef 19.2.12 → 19.3.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile +11 -16
- data/README.md +6 -1
- data/Rakefile +1 -0
- data/chef-universal-mingw-ucrt.gemspec +9 -2
- data/chef.gemspec +18 -8
- data/lib/chef/application/client.rb +7 -1
- data/lib/chef/chef_fs/file_system/chef_server/cookbook_dir.rb +1 -1
- data/lib/chef/client.rb +22 -4
- data/lib/chef/compliance/runner.rb +19 -1
- data/lib/chef/cookbook/gem_installer.rb +1 -1
- data/lib/chef/cookbook_uploader.rb +1 -1
- data/lib/chef/dsl/rest_resource.rb +63 -12
- data/lib/chef/file_access_control/windows.rb +6 -0
- data/lib/chef/file_access_control.rb +12 -1
- data/lib/chef/handler/slow_report.rb +3 -4
- data/lib/chef/licensing.rb +26 -6
- data/lib/chef/node.rb +13 -1
- data/lib/chef/policy_builder/expand_node_object.rb +12 -1
- data/lib/chef/policy_builder/policyfile.rb +12 -0
- data/lib/chef/property.rb +1 -1
- data/lib/chef/provider/file/content.rb +3 -2
- data/lib/chef/provider/file.rb +5 -2
- data/lib/chef/provider/ifconfig/redhat.rb +1 -1
- data/lib/chef/provider/package/dnf/dnf_helper.py +355 -65
- data/lib/chef/provider/package/dnf/python_helper.rb +6 -3
- data/lib/chef/provider/package/dnf.rb +24 -5
- data/lib/chef/provider/package/yum.rb +1 -1
- data/lib/chef/provider/package/yum_tm.rb +1 -1
- data/lib/chef/resource/_rest_resource.rb +4 -2
- data/lib/chef/resource/build_essential.rb +10 -1
- data/lib/chef/resource/execute.rb +0 -15
- data/lib/chef/resource/yum_package.rb +1 -1
- data/lib/chef/target_io/support.rb +1 -1
- data/lib/chef/target_io/train/dir.rb +1 -1
- data/lib/chef/target_io/train/file.rb +5 -5
- data/lib/chef/target_io/train/fileutils.rb +1 -1
- data/lib/chef/version.rb +1 -1
- data/lib/chef/win32/version.rb +17 -16
- metadata +37 -13
data/lib/chef/property.rb
CHANGED
|
@@ -698,7 +698,7 @@ class Chef
|
|
|
698
698
|
# Weeding out class methods avoids unnecessary deprecations such Chef::Resource
|
|
699
699
|
# defining a `name` property when there's an already-existing `name` method
|
|
700
700
|
# for a Module.
|
|
701
|
-
return false unless declared_in.
|
|
701
|
+
return false unless declared_in.method_defined?(name)
|
|
702
702
|
|
|
703
703
|
# Only emit deprecations for some well-known classes. This will still
|
|
704
704
|
# allow more advanced users to subclass their own custom resources and
|
|
@@ -24,9 +24,10 @@ class Chef
|
|
|
24
24
|
class File
|
|
25
25
|
class Content < Chef::FileContentManagement::ContentBase
|
|
26
26
|
def file_for_provider
|
|
27
|
-
|
|
27
|
+
content = @new_resource.content
|
|
28
|
+
if content
|
|
28
29
|
tempfile = Chef::FileContentManagement::Tempfile.new(@new_resource).tempfile
|
|
29
|
-
tempfile.write(
|
|
30
|
+
tempfile.write(content)
|
|
30
31
|
tempfile.close
|
|
31
32
|
tempfile
|
|
32
33
|
else
|
data/lib/chef/provider/file.rb
CHANGED
|
@@ -197,7 +197,7 @@ class Chef
|
|
|
197
197
|
# be overridden in subclasses.
|
|
198
198
|
def managing_content?
|
|
199
199
|
return true if new_resource.checksum
|
|
200
|
-
return true if
|
|
200
|
+
return true if new_resource.property_is_set?(:content) && @action != :create_if_missing
|
|
201
201
|
|
|
202
202
|
false
|
|
203
203
|
end
|
|
@@ -472,11 +472,14 @@ class Chef
|
|
|
472
472
|
end
|
|
473
473
|
|
|
474
474
|
def load_resource_attributes_from_file(resource)
|
|
475
|
-
if ChefUtils.windows?
|
|
475
|
+
if ChefUtils.windows? && !Chef::Config.target_mode?
|
|
476
476
|
# This is a work around for CHEF-3554.
|
|
477
477
|
# OC-6534: is tracking the real fix for this workaround.
|
|
478
478
|
# Add support for Windows equivalent, or implicit resource
|
|
479
479
|
# reporting won't work for Windows.
|
|
480
|
+
# In target mode on Windows, we still read remote attributes via
|
|
481
|
+
# TargetIO (ScanAccessControl is TargetIO-aware), so idempotency
|
|
482
|
+
# checks work correctly when targeting a remote Linux host.
|
|
480
483
|
return
|
|
481
484
|
end
|
|
482
485
|
|
|
@@ -22,7 +22,7 @@ class Chef
|
|
|
22
22
|
class Provider
|
|
23
23
|
class Ifconfig
|
|
24
24
|
class Redhat < Chef::Provider::Ifconfig
|
|
25
|
-
provides :ifconfig, platform_family:
|
|
25
|
+
provides :ifconfig, platform_family: %w{fedora rhel amazon}, target_mode: true
|
|
26
26
|
|
|
27
27
|
def initialize(new_resource, run_context)
|
|
28
28
|
super(new_resource, run_context)
|
|
@@ -1,22 +1,97 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
#
|
|
2
|
+
#
|
|
3
|
+
# Copyright:: Copyright (c) 2009-2026 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
|
|
4
|
+
# Copyright:: Copyright (c) 2026 Meta Platforms, Inc.
|
|
5
|
+
# Copyright:: Copyright (c) 2026 Phil Dibowitz
|
|
6
|
+
# License:: Apache License, Version 2.0
|
|
7
|
+
#
|
|
8
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
9
|
+
# you may not use this file except in compliance with the License.
|
|
10
|
+
# You may obtain a copy of the License at
|
|
11
|
+
#
|
|
12
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
13
|
+
#
|
|
14
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
15
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
16
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
17
|
+
# See the License for the specific language governing permissions and
|
|
18
|
+
# limitations under the License.
|
|
19
|
+
#
|
|
3
20
|
|
|
4
21
|
import sys
|
|
5
|
-
import dnf
|
|
6
|
-
import hawkey
|
|
7
22
|
import signal
|
|
8
23
|
import os
|
|
9
24
|
import json
|
|
10
25
|
|
|
26
|
+
# to enable debug logging, set the CHEF_DNF_HELPER_DEBUG_FILE environment
|
|
27
|
+
# variable to a file path
|
|
28
|
+
DEBUG_FILE = os.environ.get("CHEF_DNF_HELPER_DEBUG_FILE", None)
|
|
29
|
+
|
|
30
|
+
# Try to import dnf5 first, fall back to dnf4
|
|
31
|
+
try:
|
|
32
|
+
import libdnf5
|
|
33
|
+
import rpm
|
|
34
|
+
|
|
35
|
+
DNF_VERSION = 5
|
|
36
|
+
except ImportError:
|
|
37
|
+
try:
|
|
38
|
+
import dnf
|
|
39
|
+
import hawkey
|
|
40
|
+
|
|
41
|
+
DNF_VERSION = 4
|
|
42
|
+
except ImportError:
|
|
43
|
+
raise RuntimeError(
|
|
44
|
+
"Neither dnf5 (libdnf5) nor dnf4 (dnf) libraries are available"
|
|
45
|
+
)
|
|
46
|
+
|
|
11
47
|
base = None
|
|
12
48
|
|
|
13
|
-
|
|
49
|
+
|
|
50
|
+
def get_base_dnf5(command):
|
|
51
|
+
global base
|
|
52
|
+
if base is None:
|
|
53
|
+
base = libdnf5.base.Base()
|
|
54
|
+
|
|
55
|
+
# Load configuration
|
|
56
|
+
base.load_config()
|
|
57
|
+
|
|
58
|
+
# Set up vars
|
|
59
|
+
base.setup()
|
|
60
|
+
|
|
61
|
+
# Load repositories
|
|
62
|
+
repo_sack = base.get_repo_sack()
|
|
63
|
+
repo_sack.create_repos_from_system_configuration()
|
|
64
|
+
|
|
65
|
+
if "repos" in command:
|
|
66
|
+
for repo_pattern in command["repos"]:
|
|
67
|
+
if "enable" in repo_pattern:
|
|
68
|
+
query = libdnf5.repo.RepoQuery(base)
|
|
69
|
+
query.filter_id(
|
|
70
|
+
repo_pattern["enable"], libdnf5.common.QueryCmp_GLOB
|
|
71
|
+
)
|
|
72
|
+
for repo in query:
|
|
73
|
+
repo.enable()
|
|
74
|
+
if "disable" in repo_pattern:
|
|
75
|
+
query = libdnf5.repo.RepoQuery(base)
|
|
76
|
+
query.filter_id(
|
|
77
|
+
repo_pattern["disable"], libdnf5.common.QueryCmp_GLOB
|
|
78
|
+
)
|
|
79
|
+
for repo in query:
|
|
80
|
+
repo.disable()
|
|
81
|
+
|
|
82
|
+
# Load repositories and create solv files
|
|
83
|
+
repo_sack.load_repos()
|
|
84
|
+
|
|
85
|
+
return base
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def get_sack_dnf4(command):
|
|
14
89
|
global base
|
|
15
90
|
if base is None:
|
|
16
91
|
base = dnf.Base()
|
|
17
92
|
conf = base.conf
|
|
18
93
|
conf.read()
|
|
19
|
-
conf.installroot =
|
|
94
|
+
conf.installroot = "/"
|
|
20
95
|
conf.assumeyes = True
|
|
21
96
|
subst = conf.substitutions
|
|
22
97
|
subst.update_from_etc(conf.installroot)
|
|
@@ -28,136 +103,350 @@ def get_sack():
|
|
|
28
103
|
base.read_all_repos()
|
|
29
104
|
repos = base.repos
|
|
30
105
|
|
|
31
|
-
if
|
|
32
|
-
for repo_pattern in command[
|
|
33
|
-
if
|
|
34
|
-
for repo in repos.get_matching(repo_pattern[
|
|
106
|
+
if "repos" in command:
|
|
107
|
+
for repo_pattern in command["repos"]:
|
|
108
|
+
if "enable" in repo_pattern:
|
|
109
|
+
for repo in repos.get_matching(repo_pattern["enable"]):
|
|
35
110
|
repo.enable()
|
|
36
|
-
if
|
|
37
|
-
for repo in repos.get_matching(repo_pattern[
|
|
111
|
+
if "disable" in repo_pattern:
|
|
112
|
+
for repo in repos.get_matching(repo_pattern["disable"]):
|
|
38
113
|
repo.disable()
|
|
39
114
|
|
|
40
115
|
try:
|
|
41
116
|
base.configure_plugins()
|
|
42
117
|
except AttributeError:
|
|
43
118
|
pass
|
|
44
|
-
base.fill_sack(load_system_repo=
|
|
119
|
+
base.fill_sack(load_system_repo="auto")
|
|
45
120
|
return base.sack
|
|
46
121
|
|
|
47
|
-
|
|
48
|
-
def
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
122
|
+
|
|
123
|
+
def get_sack(command):
|
|
124
|
+
if DNF_VERSION == 5:
|
|
125
|
+
return get_base_dnf5(command)
|
|
126
|
+
else:
|
|
127
|
+
return get_sack_dnf4(command)
|
|
128
|
+
|
|
54
129
|
|
|
55
130
|
def version_tuple(versionstr):
|
|
56
|
-
e =
|
|
131
|
+
e = "0"
|
|
57
132
|
v = None
|
|
58
133
|
r = None
|
|
59
|
-
colon_index = versionstr.find(
|
|
134
|
+
colon_index = versionstr.find(":")
|
|
60
135
|
if colon_index > 0:
|
|
61
136
|
e = str(versionstr[:colon_index])
|
|
62
|
-
dash_index = versionstr.find(
|
|
137
|
+
dash_index = versionstr.find("-")
|
|
63
138
|
if dash_index > 0:
|
|
64
|
-
tmp = versionstr[colon_index + 1:dash_index]
|
|
65
|
-
if tmp !=
|
|
139
|
+
tmp = versionstr[colon_index + 1 : dash_index]
|
|
140
|
+
if tmp != "":
|
|
66
141
|
v = tmp
|
|
67
|
-
arch_index = versionstr.rfind(
|
|
142
|
+
arch_index = versionstr.rfind(".", dash_index)
|
|
68
143
|
if arch_index > 0:
|
|
69
|
-
r = versionstr[dash_index + 1:arch_index]
|
|
144
|
+
r = versionstr[dash_index + 1 : arch_index]
|
|
70
145
|
else:
|
|
71
|
-
r = versionstr[dash_index + 1:]
|
|
146
|
+
r = versionstr[dash_index + 1 :]
|
|
72
147
|
else:
|
|
73
|
-
tmp = versionstr[colon_index + 1:]
|
|
74
|
-
if tmp !=
|
|
148
|
+
tmp = versionstr[colon_index + 1 :]
|
|
149
|
+
if tmp != "":
|
|
75
150
|
v = tmp
|
|
76
151
|
return (e, v, r)
|
|
77
152
|
|
|
78
|
-
|
|
79
|
-
|
|
153
|
+
|
|
154
|
+
# If we pass in 0:1.10 and 1.2 to the dnf5 libraries, it won't compare
|
|
155
|
+
# them correctly, they both need to have epochs or not have epochs. However,
|
|
156
|
+
# unlike dnf4 libraries, it they don't take tuples, so use version_tuple to
|
|
157
|
+
# canonicalize the parts of the version, then reassemble them into a full EVR
|
|
158
|
+
# string.
|
|
159
|
+
def version_canonicalize(versionstr):
|
|
160
|
+
e, v, r = version_tuple(versionstr)
|
|
161
|
+
return f"{e}:{v}-{r}"
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def versioncompare(command):
|
|
165
|
+
versions = command["versions"]
|
|
166
|
+
sack = get_sack(command)
|
|
80
167
|
if (versions[0] is None) or (versions[1] is None):
|
|
81
|
-
outpipe.write(
|
|
168
|
+
outpipe.write("0\n")
|
|
82
169
|
outpipe.flush()
|
|
83
170
|
else:
|
|
84
|
-
|
|
85
|
-
|
|
171
|
+
if DNF_VERSION == 4:
|
|
172
|
+
evr_comparison = dnf.rpm.rpm.labelCompare(
|
|
173
|
+
version_tuple(versions[0]), version_tuple(versions[1])
|
|
174
|
+
)
|
|
175
|
+
outpipe.write("{}\n".format(evr_comparison))
|
|
176
|
+
else:
|
|
177
|
+
# dnf5 version comparison - rpmvercmp handles full EVR strings
|
|
178
|
+
cmp_result = libdnf5.rpm.rpmvercmp(
|
|
179
|
+
version_canonicalize(versions[0]),
|
|
180
|
+
version_canonicalize(versions[1]),
|
|
181
|
+
)
|
|
182
|
+
outpipe.write("{}\n".format(cmp_result))
|
|
86
183
|
outpipe.flush()
|
|
87
184
|
|
|
88
|
-
def query(command):
|
|
89
|
-
sack = get_sack()
|
|
90
185
|
|
|
91
|
-
|
|
186
|
+
def query_dnf4(command):
|
|
187
|
+
sack = get_sack(command)
|
|
188
|
+
|
|
189
|
+
subj = dnf.subject.Subject(command["provides"])
|
|
92
190
|
q = subj.get_best_query(sack, with_provides=True)
|
|
93
191
|
|
|
94
|
-
if command[
|
|
192
|
+
if command["action"] == "whatinstalled":
|
|
95
193
|
# When attempting to figure out what is installed, we should ignore any
|
|
96
194
|
# excludes that are configured, otherwise the "best" query for a given
|
|
97
195
|
# subject may refer to a package that is installed that provides that
|
|
98
196
|
# subject, but we really want to know if a package by that name exists
|
|
99
197
|
# in any available repository
|
|
100
|
-
q = subj.get_best_query(
|
|
101
|
-
|
|
198
|
+
q = subj.get_best_query(
|
|
199
|
+
sack,
|
|
200
|
+
with_provides=True,
|
|
201
|
+
query=sack.query(flags=hawkey.IGNORE_EXCLUDES),
|
|
202
|
+
)
|
|
102
203
|
q = q.installed()
|
|
103
204
|
|
|
104
|
-
if command[
|
|
205
|
+
if command["action"] == "whatavailable":
|
|
105
206
|
q = q.available()
|
|
106
207
|
|
|
107
|
-
if
|
|
208
|
+
if "epoch" in command:
|
|
108
209
|
# We assume that any glob is "*" so just omit the filter since the dnf libraries have no
|
|
109
210
|
# epoch__glob filter. That means "?" wildcards in epochs will fail. The workaround is to
|
|
110
211
|
# not use the version filter here but to put the version with all the globs in the package name.
|
|
111
|
-
if not dnf.util.is_glob_pattern(command[
|
|
112
|
-
q = q.filterm(epoch=int(command[
|
|
113
|
-
if
|
|
114
|
-
if dnf.util.is_glob_pattern(command[
|
|
115
|
-
q = q.filterm(version__glob=command[
|
|
212
|
+
if not dnf.util.is_glob_pattern(command["epoch"]):
|
|
213
|
+
q = q.filterm(epoch=int(command["epoch"]))
|
|
214
|
+
if "version" in command:
|
|
215
|
+
if dnf.util.is_glob_pattern(command["version"]):
|
|
216
|
+
q = q.filterm(version__glob=command["version"])
|
|
116
217
|
else:
|
|
117
|
-
q = q.filterm(version=command[
|
|
118
|
-
if
|
|
119
|
-
if dnf.util.is_glob_pattern(command[
|
|
120
|
-
q = q.filterm(release__glob=command[
|
|
218
|
+
q = q.filterm(version=command["version"])
|
|
219
|
+
if "release" in command:
|
|
220
|
+
if dnf.util.is_glob_pattern(command["release"]):
|
|
221
|
+
q = q.filterm(release__glob=command["release"])
|
|
121
222
|
else:
|
|
122
|
-
q = q.filterm(release=command[
|
|
223
|
+
q = q.filterm(release=command["release"])
|
|
123
224
|
|
|
124
|
-
if
|
|
125
|
-
if dnf.util.is_glob_pattern(command[
|
|
126
|
-
q = q.filterm(arch__glob=command[
|
|
225
|
+
if "arch" in command:
|
|
226
|
+
if dnf.util.is_glob_pattern(command["arch"]):
|
|
227
|
+
q = q.filterm(arch__glob=command["arch"])
|
|
127
228
|
else:
|
|
128
|
-
q = q.filterm(arch=command[
|
|
229
|
+
q = q.filterm(arch=command["arch"])
|
|
129
230
|
|
|
130
231
|
# only apply the default arch query filter if it returns something
|
|
131
|
-
archq = q.filter(arch=[
|
|
232
|
+
archq = q.filter(arch=["noarch", hawkey.detect_arch()])
|
|
132
233
|
if len(archq.run()) > 0:
|
|
133
234
|
q = archq
|
|
134
235
|
|
|
135
236
|
pkgs = q.latest(1).run()
|
|
136
237
|
|
|
137
238
|
if not pkgs:
|
|
138
|
-
outpipe.write(
|
|
239
|
+
outpipe.write("{} nil nil\n".format(command["provides"].split().pop(0)))
|
|
139
240
|
outpipe.flush()
|
|
140
241
|
else:
|
|
141
242
|
# make sure we picked the package with the highest version
|
|
142
243
|
pkgs.sort
|
|
143
244
|
pkg = pkgs.pop()
|
|
144
|
-
outpipe.write(
|
|
245
|
+
outpipe.write(
|
|
246
|
+
"{} {}:{}-{} {}\n".format(
|
|
247
|
+
pkg.name, pkg.epoch, pkg.version, pkg.release, pkg.arch
|
|
248
|
+
)
|
|
249
|
+
)
|
|
250
|
+
outpipe.flush()
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def log(message):
|
|
254
|
+
if DEBUG_FILE is None:
|
|
255
|
+
return
|
|
256
|
+
with open(DEBUG_FILE, "a") as f:
|
|
257
|
+
f.write(message + "\n")
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def query_dnf5(command):
|
|
261
|
+
"""
|
|
262
|
+
Query dnf5 for package information based on the command dict.
|
|
263
|
+
|
|
264
|
+
This method does a fair amount of work to try to mimic the behavior
|
|
265
|
+
of "dnf install <foo>". In the DNF4 world, this functionality was
|
|
266
|
+
exposed through the dnf.subject.Subject class. In DNF5, this functionality
|
|
267
|
+
is internal to the Goal class, which you can use, but then you can't get
|
|
268
|
+
a list of matching packages out of - you can simply ask the goal to be
|
|
269
|
+
resolved to a package transaction, and then run or not run that transaction.
|
|
270
|
+
|
|
271
|
+
So instead we combine the nevra filtering and provides filtering to mimic
|
|
272
|
+
the behavior of being able to handle anything that could be passed to
|
|
273
|
+
"dnf install <foo>".
|
|
274
|
+
|
|
275
|
+
Some of the cases we handle are:
|
|
276
|
+
- name only: "foo"
|
|
277
|
+
- name and arch: "foo.x86_64"
|
|
278
|
+
- name and version: "foo-1.2"
|
|
279
|
+
- name, version, release: "foo-1.2-3"
|
|
280
|
+
- name, version, release, arch: "foo-1.2-3.x
|
|
281
|
+
- name with version constraint: "foo >= 1.2"
|
|
282
|
+
- globs: "foo*", "foo-1.2*", "foo-1.2-3*", "foo-1*.*", etc.
|
|
283
|
+
|
|
284
|
+
A full exercising of this functionality testing all known cases is
|
|
285
|
+
in the unittest for the DNF provider.
|
|
286
|
+
"""
|
|
287
|
+
base = get_sack(command)
|
|
288
|
+
q = libdnf5.rpm.PackageQuery(base)
|
|
289
|
+
|
|
290
|
+
# First, we need to know if this parses as a nevra or not, which will
|
|
291
|
+
# inform the rest of our decision tree.
|
|
292
|
+
provides_str = command["provides"]
|
|
293
|
+
try:
|
|
294
|
+
nevra_vector = libdnf5.rpm.Nevra.parse(provides_str)
|
|
295
|
+
except libdnf5.exception.RpmNevraIncorrectInputError:
|
|
296
|
+
# when parse() throws an this exception, it's because there's spaces,
|
|
297
|
+
# or other special characters in it, and the only valid things passed
|
|
298
|
+
# to us that fit that category are constrains like: "foo >= 1.2". So
|
|
299
|
+
# parse it as one of those, add the constraint to the query, and update
|
|
300
|
+
# the name we search for to the parsed name
|
|
301
|
+
nevra_vector = []
|
|
302
|
+
reldep = libdnf5.rpm.Reldep(base, provides_str)
|
|
303
|
+
provides_str = reldep.get_name()
|
|
304
|
+
q.filter_provides(reldep)
|
|
305
|
+
|
|
306
|
+
# unlike the old subject based query, filter_nevra doesn't handle
|
|
307
|
+
# the <name>.<arch> case properly. Further, adding * to arch causes
|
|
308
|
+
# weirdness. So, we detect the arch suffix, and strip it off and add
|
|
309
|
+
# it to the direct arch filter.
|
|
310
|
+
#
|
|
311
|
+
# Unfortunately, since we want to support nearly any possible combination
|
|
312
|
+
# of name, version, release, arch with globs, we have to do some extra work
|
|
313
|
+
# here. parse() will give us an iterable list of possible interpretations
|
|
314
|
+
# of the string. That can include dumb things like for "foo-1.2" the
|
|
315
|
+
# possibility that "2" is an arch. So, we take the arch and see if it's
|
|
316
|
+
# a compatible arch with us (e.g. x86_64 and i686 on x86_64 systems). If
|
|
317
|
+
# we find one that matches, we use that, rip the arch off, add it to the
|
|
318
|
+
# filters.
|
|
319
|
+
#
|
|
320
|
+
# While there may be other entries in the list that are (more) correct,
|
|
321
|
+
# it doesn't matter, we're only need to detect if a valid arch was specified
|
|
322
|
+
# so we can handle that manually.
|
|
323
|
+
nevra = None
|
|
324
|
+
for n in nevra_vector:
|
|
325
|
+
log(
|
|
326
|
+
f" => Possible interpretation: n:{n.get_name()} v:{n.get_version()} r:{n.get_release()} a:{n.get_arch()}"
|
|
327
|
+
)
|
|
328
|
+
arch = n.get_arch()
|
|
329
|
+
if arch != "" and rpm.archscore(arch) > 0:
|
|
330
|
+
log(f" => Selected interpretation with arch: {arch}")
|
|
331
|
+
nevra = n
|
|
332
|
+
break
|
|
333
|
+
|
|
334
|
+
# if we found a nevra with a valid arch, use that arch
|
|
335
|
+
if nevra is not None:
|
|
336
|
+
arch = nevra.get_arch()
|
|
337
|
+
name = nevra.get_name()
|
|
338
|
+
if arch and provides_str.endswith(arch):
|
|
339
|
+
command["arch"] = nevra.get_arch()
|
|
340
|
+
# strip of ".<arch>" from the end of provides_str
|
|
341
|
+
provides_str = provides_str[: -(len(arch) + 1)]
|
|
342
|
+
|
|
343
|
+
# in order to get the behavior of "dnf install <blah>" we have to add
|
|
344
|
+
# '*' to the end in order to make stuff like "chef_rpm-1.2" work.
|
|
345
|
+
if not provides_str.endswith("*"):
|
|
346
|
+
provides_str += "*"
|
|
347
|
+
|
|
348
|
+
log(f" => provides_str after processing: {provides_str}")
|
|
349
|
+
log(f" => command after processing: {command}")
|
|
350
|
+
if command["action"] == "whatinstalled":
|
|
351
|
+
q.filter_installed()
|
|
352
|
+
|
|
353
|
+
if command["action"] == "whatavailable":
|
|
354
|
+
q.filter_available()
|
|
355
|
+
|
|
356
|
+
# Apply version filters
|
|
357
|
+
if "epoch" in command:
|
|
358
|
+
if "*" not in command["epoch"] and "?" not in command["epoch"]:
|
|
359
|
+
q.filter_epoch(int(command["epoch"]))
|
|
360
|
+
|
|
361
|
+
if "version" in command:
|
|
362
|
+
if "*" in command["version"] or "?" in command["version"]:
|
|
363
|
+
q.filter_version(command["version"], libdnf5.common.QueryCmp_GLOB)
|
|
364
|
+
else:
|
|
365
|
+
q.filter_version(command["version"])
|
|
366
|
+
|
|
367
|
+
if "release" in command:
|
|
368
|
+
if "*" in command["release"] or "?" in command["release"]:
|
|
369
|
+
q.filter_release(command["release"], libdnf5.common.QueryCmp_GLOB)
|
|
370
|
+
else:
|
|
371
|
+
q.filter_release(command["release"])
|
|
372
|
+
|
|
373
|
+
if "arch" in command:
|
|
374
|
+
if "*" in command["arch"] or "?" in command["arch"]:
|
|
375
|
+
q.filter_arch(command["arch"], libdnf5.common.QueryCmp_GLOB)
|
|
376
|
+
else:
|
|
377
|
+
q.filter_arch(command["arch"])
|
|
378
|
+
|
|
379
|
+
# now, we try by nevra search, and *IF* that returns nothing, then
|
|
380
|
+
# do a provides search. Combined with the work above to handle various
|
|
381
|
+
# name conventions, this gets is roughly compatible with the old
|
|
382
|
+
# dnf4 "subject" calls.
|
|
383
|
+
nevra_q = libdnf5.rpm.PackageQuery(q)
|
|
384
|
+
nevra_q.filter_nevra(provides_str, libdnf5.common.QueryCmp_GLOB)
|
|
385
|
+
if not nevra_q.empty():
|
|
386
|
+
q = nevra_q
|
|
387
|
+
else:
|
|
388
|
+
q.filter_provides(provides_str, libdnf5.common.QueryCmp_GLOB)
|
|
389
|
+
|
|
390
|
+
# Filter by architecture (prefer noarch and native arch)
|
|
391
|
+
# Get the system architecture from vars
|
|
392
|
+
detected_arch = base.get_vars().get_value("arch")
|
|
393
|
+
archq = libdnf5.rpm.PackageQuery(q)
|
|
394
|
+
archq.filter_arch(["noarch", detected_arch])
|
|
395
|
+
|
|
396
|
+
if not archq.empty():
|
|
397
|
+
q = archq
|
|
398
|
+
|
|
399
|
+
# Get latest packages
|
|
400
|
+
q.filter_latest_evr()
|
|
401
|
+
|
|
402
|
+
pkgs = list(q)
|
|
403
|
+
log(f" => pkgs from query: {pkgs}")
|
|
404
|
+
|
|
405
|
+
if not pkgs:
|
|
406
|
+
outpipe.write("{} nil nil\n".format(command["provides"].split().pop(0)))
|
|
145
407
|
outpipe.flush()
|
|
408
|
+
else:
|
|
409
|
+
# Sort and get the highest version
|
|
410
|
+
pkgs.sort(
|
|
411
|
+
key=lambda p: (p.get_epoch(), p.get_version(), p.get_release()),
|
|
412
|
+
reverse=True,
|
|
413
|
+
)
|
|
414
|
+
pkg = pkgs[0]
|
|
415
|
+
outpipe.write(
|
|
416
|
+
"{} {}:{}-{} {}\n".format(
|
|
417
|
+
pkg.get_name(),
|
|
418
|
+
pkg.get_epoch(),
|
|
419
|
+
pkg.get_version(),
|
|
420
|
+
pkg.get_release(),
|
|
421
|
+
pkg.get_arch(),
|
|
422
|
+
)
|
|
423
|
+
)
|
|
424
|
+
outpipe.flush()
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def query(command):
|
|
428
|
+
if DNF_VERSION == 5:
|
|
429
|
+
query_dnf5(command)
|
|
430
|
+
else:
|
|
431
|
+
query_dnf4(command)
|
|
432
|
+
|
|
146
433
|
|
|
147
434
|
# the design of this helper is that it should try to be 'brittle' and fail hard and exit in order
|
|
148
435
|
# to keep process tables clean. additional error handling should probably be added to the retry loop
|
|
149
436
|
# on the ruby side.
|
|
150
437
|
def exit_handler(signal, frame):
|
|
151
|
-
if base is not None:
|
|
438
|
+
if DNF_VERSION == 4 and base is not None:
|
|
152
439
|
base.close()
|
|
153
440
|
sys.exit(0)
|
|
154
441
|
|
|
442
|
+
|
|
155
443
|
def setup_exit_handler():
|
|
156
444
|
signal.signal(signal.SIGINT, exit_handler)
|
|
157
445
|
signal.signal(signal.SIGHUP, exit_handler)
|
|
158
446
|
signal.signal(signal.SIGPIPE, exit_handler)
|
|
159
447
|
signal.signal(signal.SIGQUIT, exit_handler)
|
|
160
448
|
|
|
449
|
+
|
|
161
450
|
if len(sys.argv) < 3:
|
|
162
451
|
inpipe = sys.stdin
|
|
163
452
|
outpipe = sys.stdout
|
|
@@ -185,14 +474,15 @@ try:
|
|
|
185
474
|
except ValueError:
|
|
186
475
|
raise RuntimeError("bad json parse")
|
|
187
476
|
|
|
188
|
-
|
|
477
|
+
log(f"COMMAND: {command}")
|
|
478
|
+
if command["action"] == "whatinstalled":
|
|
189
479
|
query(command)
|
|
190
|
-
elif command[
|
|
480
|
+
elif command["action"] == "whatavailable":
|
|
191
481
|
query(command)
|
|
192
|
-
elif command[
|
|
193
|
-
versioncompare(command
|
|
482
|
+
elif command["action"] == "versioncompare":
|
|
483
|
+
versioncompare(command)
|
|
194
484
|
else:
|
|
195
485
|
raise RuntimeError("bad command")
|
|
196
486
|
finally:
|
|
197
|
-
if base is not None:
|
|
487
|
+
if DNF_VERSION == 4 and base is not None:
|
|
198
488
|
base.close()
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
1
2
|
#
|
|
2
3
|
# Copyright:: Copyright (c) 2009-2026 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
|
|
4
|
+
# Copyright:: Copyright (c) 2026 Meta Platforms, Inc.
|
|
5
|
+
# Copyright:: Copyright (c) 2026 Phil Dibowitz
|
|
3
6
|
# License:: Apache License, Version 2.0
|
|
4
7
|
#
|
|
5
8
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
@@ -41,10 +44,10 @@ class Chef
|
|
|
41
44
|
|
|
42
45
|
def dnf_command
|
|
43
46
|
# platform-python is used for system tools on RHEL 8 and is installed under /usr/libexec
|
|
47
|
+
py_cmd = "try:\n import libdnf5\nexcept ImportError:\n import dnf"
|
|
44
48
|
@dnf_command ||= begin
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
end
|
|
49
|
+
executables = where("platform-python", "python", "python3", "python2", "python2.7", extra_path: "/usr/libexec")
|
|
50
|
+
cmd = executables.find { |f| shell_out("#{f} -c '#{py_cmd}'").exitstatus == 0 }
|
|
48
51
|
raise Chef::Exceptions::Package, "cannot find dnf libraries, you may need to use yum_package" unless cmd
|
|
49
52
|
|
|
50
53
|
"#{cmd} #{DNF_HELPER}"
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
#
|
|
2
2
|
# Copyright:: Copyright (c) 2009-2026 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
|
|
3
|
+
# Copyright:: Copyright (c) 2026 Meta Platforms, Inc.
|
|
4
|
+
# Copyright:: Copyright (c) 2026 Phil Dibowitz
|
|
3
5
|
# License:: Apache License, Version 2.0
|
|
4
6
|
#
|
|
5
7
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
@@ -62,6 +64,13 @@ class Chef
|
|
|
62
64
|
@python_helper ||= PythonHelper.instance
|
|
63
65
|
end
|
|
64
66
|
|
|
67
|
+
def dnf5?
|
|
68
|
+
@dnf5 ||= begin
|
|
69
|
+
dnf_version = shell_out!("dnf --version").stdout
|
|
70
|
+
dnf_version =~ /dnf5/i
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
65
74
|
def load_current_resource
|
|
66
75
|
flushcache if new_resource.flush_cache[:before]
|
|
67
76
|
|
|
@@ -138,14 +147,19 @@ class Chef
|
|
|
138
147
|
# NB: the dnf_package provider manages individual single packages, please do not submit issues or PRs to try to add wildcard
|
|
139
148
|
# support to lock / unlock. The best solution is to write an execute resource which does a not_if `dnf versionlock | grep '^pattern`` kind of approach
|
|
140
149
|
def lock_package(names, versions)
|
|
141
|
-
|
|
150
|
+
default_opts = dnf5? ? [] : %w{-d0 -e0}
|
|
151
|
+
dnf(default_opts, options, "versionlock", "add", resolved_package_lock_names(names))
|
|
142
152
|
end
|
|
143
153
|
|
|
144
154
|
# NB: the dnf_package provider manages individual single packages, please do not submit issues or PRs to try to add wildcard
|
|
145
155
|
# support to lock / unlock. The best solution is to write an execute resource which does a only_if `dnf versionlock | grep '^pattern`` kind of approach
|
|
146
156
|
def unlock_package(names, versions)
|
|
147
|
-
|
|
148
|
-
|
|
157
|
+
if dnf5?
|
|
158
|
+
dnf("-y", options, "versionlock", "delete", resolved_package_lock_names(names))
|
|
159
|
+
else
|
|
160
|
+
# dnf versionlock delete on rhel6 needs the glob nonsense in the following command
|
|
161
|
+
dnf("-d0", "-e0", "-y", options, "versionlock", "delete", resolved_package_lock_names(names).map { |n| "*:#{n}-*" })
|
|
162
|
+
end
|
|
149
163
|
end
|
|
150
164
|
|
|
151
165
|
private
|
|
@@ -167,8 +181,13 @@ class Chef
|
|
|
167
181
|
@locked_packages ||=
|
|
168
182
|
begin
|
|
169
183
|
locked = dnf("versionlock", "list")
|
|
170
|
-
|
|
171
|
-
|
|
184
|
+
if dnf5?
|
|
185
|
+
locked.stdout.each_line.select { |x| x.start_with?("Package name:") }
|
|
186
|
+
.map { |line| line.split(": ").last.strip }
|
|
187
|
+
else
|
|
188
|
+
locked.stdout.each_line.map do |line|
|
|
189
|
+
line.sub(/-[^-]*-[^-]*$/, "").split(":").last.strip
|
|
190
|
+
end
|
|
172
191
|
end
|
|
173
192
|
end
|
|
174
193
|
end
|