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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +11 -16
  3. data/README.md +6 -1
  4. data/Rakefile +1 -0
  5. data/chef-universal-mingw-ucrt.gemspec +9 -2
  6. data/chef.gemspec +18 -8
  7. data/lib/chef/application/client.rb +7 -1
  8. data/lib/chef/chef_fs/file_system/chef_server/cookbook_dir.rb +1 -1
  9. data/lib/chef/client.rb +22 -4
  10. data/lib/chef/compliance/runner.rb +19 -1
  11. data/lib/chef/cookbook/gem_installer.rb +1 -1
  12. data/lib/chef/cookbook_uploader.rb +1 -1
  13. data/lib/chef/dsl/rest_resource.rb +63 -12
  14. data/lib/chef/file_access_control/windows.rb +6 -0
  15. data/lib/chef/file_access_control.rb +12 -1
  16. data/lib/chef/handler/slow_report.rb +3 -4
  17. data/lib/chef/licensing.rb +26 -6
  18. data/lib/chef/node.rb +13 -1
  19. data/lib/chef/policy_builder/expand_node_object.rb +12 -1
  20. data/lib/chef/policy_builder/policyfile.rb +12 -0
  21. data/lib/chef/property.rb +1 -1
  22. data/lib/chef/provider/file/content.rb +3 -2
  23. data/lib/chef/provider/file.rb +5 -2
  24. data/lib/chef/provider/ifconfig/redhat.rb +1 -1
  25. data/lib/chef/provider/package/dnf/dnf_helper.py +355 -65
  26. data/lib/chef/provider/package/dnf/python_helper.rb +6 -3
  27. data/lib/chef/provider/package/dnf.rb +24 -5
  28. data/lib/chef/provider/package/yum.rb +1 -1
  29. data/lib/chef/provider/package/yum_tm.rb +1 -1
  30. data/lib/chef/resource/_rest_resource.rb +4 -2
  31. data/lib/chef/resource/build_essential.rb +10 -1
  32. data/lib/chef/resource/execute.rb +0 -15
  33. data/lib/chef/resource/yum_package.rb +1 -1
  34. data/lib/chef/target_io/support.rb +1 -1
  35. data/lib/chef/target_io/train/dir.rb +1 -1
  36. data/lib/chef/target_io/train/file.rb +5 -5
  37. data/lib/chef/target_io/train/fileutils.rb +1 -1
  38. data/lib/chef/version.rb +1 -1
  39. data/lib/chef/win32/version.rb +17 -16
  40. 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.instance_methods.include?(name)
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
- if @new_resource.content
27
+ content = @new_resource.content
28
+ if content
28
29
  tempfile = Chef::FileContentManagement::Tempfile.new(@new_resource).tempfile
29
- tempfile.write(@new_resource.content)
30
+ tempfile.write(content)
30
31
  tempfile.close
31
32
  tempfile
32
33
  else
@@ -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 !new_resource.content.nil? && @action != :create_if_missing
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: "fedora_derived", target_mode: true
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
- # vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4
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
- def get_sack():
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 'repos' in command:
32
- for repo_pattern in command['repos']:
33
- if 'enable' in repo_pattern:
34
- for repo in repos.get_matching(repo_pattern['enable']):
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 'disable' in repo_pattern:
37
- for repo in repos.get_matching(repo_pattern['disable']):
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='auto')
119
+ base.fill_sack(load_system_repo="auto")
45
120
  return base.sack
46
121
 
47
- # FIXME: leaks memory and does not work
48
- def flushcache():
49
- try:
50
- os.remove('/var/cache/dnf/@System.solv')
51
- except OSError:
52
- pass
53
- get_sack().load_system_repo(build_cache=True)
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 = '0'
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('.', dash_index)
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
- def versioncompare(versions):
79
- sack = get_sack()
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('0\n')
168
+ outpipe.write("0\n")
82
169
  outpipe.flush()
83
170
  else:
84
- evr_comparison = dnf.rpm.rpm.labelCompare(version_tuple(versions[0]), version_tuple(versions[1]))
85
- outpipe.write('{}\n'.format(evr_comparison))
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
- subj = dnf.subject.Subject(command['provides'])
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['action'] == "whatinstalled":
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(sack, with_provides=True, query=sack.query(flags=hawkey.IGNORE_EXCLUDES))
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['action'] == "whatavailable":
205
+ if command["action"] == "whatavailable":
105
206
  q = q.available()
106
207
 
107
- if 'epoch' in command:
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['epoch']):
112
- q = q.filterm(epoch=int(command['epoch']))
113
- if 'version' in command:
114
- if dnf.util.is_glob_pattern(command['version']):
115
- q = q.filterm(version__glob=command['version'])
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['version'])
118
- if 'release' in command:
119
- if dnf.util.is_glob_pattern(command['release']):
120
- q = q.filterm(release__glob=command['release'])
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['release'])
223
+ q = q.filterm(release=command["release"])
123
224
 
124
- if 'arch' in command:
125
- if dnf.util.is_glob_pattern(command['arch']):
126
- q = q.filterm(arch__glob=command['arch'])
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['arch'])
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=[ 'noarch', hawkey.detect_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('{} nil nil\n'.format(command['provides'].split().pop(0)))
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('{} {}:{}-{} {}\n'.format(pkg.name, pkg.epoch, pkg.version, pkg.release, pkg.arch))
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
- if command['action'] == "whatinstalled":
477
+ log(f"COMMAND: {command}")
478
+ if command["action"] == "whatinstalled":
189
479
  query(command)
190
- elif command['action'] == "whatavailable":
480
+ elif command["action"] == "whatavailable":
191
481
  query(command)
192
- elif command['action'] == "versioncompare":
193
- versioncompare(command['versions'])
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
- cmd = which("platform-python", "python", "python3", "python2", "python2.7", extra_path: "/usr/libexec") do |f|
46
- shell_out("#{f} -c 'import dnf'").exitstatus == 0
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
- dnf("-d0", "-e0", "-y", options, "versionlock", "add", resolved_package_lock_names(names))
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
- # dnf versionlock delete on rhel6 needs the glob nonsense in the following command
148
- dnf("-d0", "-e0", "-y", options, "versionlock", "delete", resolved_package_lock_names(names).map { |n| "*:#{n}-*" })
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
- locked.stdout.each_line.map do |line|
171
- line.sub(/-[^-]*-[^-]*$/, "").split(":").last.strip
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