chef 18.10.17 → 18.11.11

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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +15 -7
  3. data/README.md +0 -1
  4. data/chef-universal-mingw-ucrt.gemspec +4 -2
  5. data/chef.gemspec +15 -12
  6. data/lib/chef/compliance/runner.rb +19 -1
  7. data/lib/chef/dsl/rest_resource.rb +223 -16
  8. data/lib/chef/formatters/error_description.rb +8 -1
  9. data/lib/chef/http/authenticator.rb +17 -4
  10. data/lib/chef/mixin/template.rb +24 -2
  11. data/lib/chef/monkey_patches/reline-windows.rb +116 -0
  12. data/lib/chef/provider/file/content.rb +3 -2
  13. data/lib/chef/provider/file.rb +1 -1
  14. data/lib/chef/provider/ifconfig/redhat.rb +1 -1
  15. data/lib/chef/provider/package/dnf/dnf_helper.py +355 -65
  16. data/lib/chef/provider/package/dnf/python_helper.rb +7 -4
  17. data/lib/chef/provider/package/dnf.rb +25 -6
  18. data/lib/chef/provider/package/yum.rb +1 -1
  19. data/lib/chef/resource/_rest_resource.rb +4 -2
  20. data/lib/chef/resource/archive_file.rb +52 -18
  21. data/lib/chef/resource/chef_client_config.rb +185 -19
  22. data/lib/chef/resource/chef_vault_secret.rb +1 -1
  23. data/lib/chef/resource/chocolatey_installer.rb +31 -13
  24. data/lib/chef/resource/chocolatey_package.rb +1 -1
  25. data/lib/chef/resource/habitat/habitat_package.rb +1 -1
  26. data/lib/chef/resource/habitat/habitat_sup.rb +2 -2
  27. data/lib/chef/resource/habitat_config.rb +1 -1
  28. data/lib/chef/resource/habitat_service.rb +1 -1
  29. data/lib/chef/resource/openssl_ec_private_key.rb +1 -1
  30. data/lib/chef/resource/openssl_ec_public_key.rb +2 -2
  31. data/lib/chef/resource/openssl_rsa_private_key.rb +1 -1
  32. data/lib/chef/resource/openssl_rsa_public_key.rb +2 -2
  33. data/lib/chef/resource/openssl_x509_certificate.rb +1 -1
  34. data/lib/chef/resource/openssl_x509_request.rb +1 -1
  35. data/lib/chef/resource/powershell_package_source.rb +1 -1
  36. data/lib/chef/resource/rhsm_register.rb +1 -1
  37. data/lib/chef/resource/windows_ad_join.rb +1 -1
  38. data/lib/chef/resource/windows_certificate.rb +1 -1
  39. data/lib/chef/resource/windows_service.rb +1 -1
  40. data/lib/chef/resource/windows_task.rb +1 -1
  41. data/lib/chef/resource/yum_package.rb +1 -1
  42. data/lib/chef/resource/yum_repository.rb +2 -2
  43. data/lib/chef/shell.rb +7 -0
  44. data/lib/chef/version.rb +1 -1
  45. metadata +59 -34
@@ -0,0 +1,116 @@
1
+ # Author:: Chef Software Inc.
2
+ # Copyright:: Copyright (c) Chef Software Inc.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+ # == Background
18
+ #
19
+ # On Windows, Reline's process_key_event emits an ESC byte (0x1B) before every
20
+ # character when the :ALT control key flag is set. This is correct for true
21
+ # Alt key combos (Meta sequences), but AltGr on European keyboards – most
22
+ # notably German layouts – is reported by Windows as Ctrl+Alt (RIGHT_CTRL +
23
+ # RIGHT_ALT pressed simultaneously). Characters that require AltGr, such as
24
+ # { } [ ] @ ~ | \, therefore receive both :CTRL and :ALT flags, causing Reline to
25
+ # emit an ESC prefix. In a terminal the result looks like ^[{ instead of {.
26
+ #
27
+ # The corrupt input leaves IRB's parser in an open-string/block state which
28
+ # re-draws continuation prompts endlessly when multi-line blocks are pasted,
29
+ # making chef-shell unusable for German (and many other European) keyboard users.
30
+ #
31
+ # == Fix
32
+ #
33
+ # Only emit the ESC Meta prefix when :ALT is set WITHOUT :CTRL. When both
34
+ # flags are present the keypress is AltGr-originated and the raw character
35
+ # bytes are sufficient and correct.
36
+ #
37
+ # == Affected versions
38
+ #
39
+ # Verified against Reline <= 0.3.x (shipped with Ruby 3.1.x in chef-foundation).
40
+ # The issue was resolved upstream in Reline 0.3.2 / irb 1.6.2 (Ruby 3.2+),
41
+ # so the patch is guarded to apply only when the installed Reline version is
42
+ # affected.
43
+ #
44
+ # == References
45
+ #
46
+ # * Upstream report: https://github.com/ruby/reline/issues/475
47
+ # * Chef PR #9267 (Ruby 2.7 IRB fixes, introduced multiline/singleline wiring)
48
+ # * Chef PR #14919 (Prompt consistency fix for left-arrow on Ruby 3.1/3.2)
49
+ # * Chef PR #15336 (IRB @ALIASES removal compat)
50
+
51
+ if Gem.win_platform? && defined?(Reline::Windows)
52
+
53
+ # Only patch versions where the bug is present. Reline 0.3.2 fixed it
54
+ # upstream. Gem::Version comparison is safe even when reline is a stdlib
55
+ # default gem without a Gemspec entry in loaded_specs.
56
+ reline_version = begin
57
+ Gem::Version.new(Reline::VERSION)
58
+ rescue StandardError
59
+ Gem::Version.new("0")
60
+ end
61
+
62
+ if reline_version < Gem::Version.new("0.3.2")
63
+ Chef::Log.debug("chef-shell: applying Reline::Windows AltGr monkey patch (reline #{Reline::VERSION})")
64
+
65
+ class Reline::Windows
66
+ class << self
67
+ # Reopen process_key_event and change only the ESC-emission guard.
68
+ # All other behavior – surrogate pair handling, KEY_MAP matching, bare
69
+ # control-key suppression – is preserved verbatim from the original.
70
+ def process_key_event(repeat_count, virtual_key_code, virtual_scan_code, char_code, control_key_state)
71
+ # ---- surrogate pair handling (unchanged) --------------------------------
72
+ if char_code.between?(0xD800, 0xDBFF)
73
+ @@hsg = char_code
74
+ return
75
+ end
76
+
77
+ if char_code.between?(0xDC00, 0xDFFF)
78
+ if @@hsg
79
+ char_code = 0x10000 + (@@hsg - 0xD800) * 0x400 + char_code - 0xDC00
80
+ @@hsg = nil
81
+ else
82
+ return # low-surrogate without preceding high-surrogate – ignore
83
+ end
84
+ else
85
+ @@hsg = nil # discard stale high-surrogate
86
+ end
87
+ # -------------------------------------------------------------------------
88
+
89
+ key = KeyEventRecord.new(virtual_key_code, char_code, control_key_state)
90
+
91
+ # KEY_MAP takes priority (arrow keys, Delete, Home, End, Tab, etc.)
92
+ match = KEY_MAP.find { |args,| key.matches?(**args) }
93
+ unless match.nil?
94
+ @@output_buf.concat(match.last)
95
+ return
96
+ end
97
+
98
+ # Suppress bare modifier-only keypresses (no printable character)
99
+ return if key.char_code == 0 && key.control_keys.any?
100
+
101
+ # ---- THE FIX -----------------------------------------------------------
102
+ # Original: @@output_buf.push("\e".ord) if key.control_keys.include?(:ALT)
103
+ #
104
+ # AltGr on European keyboards reports as Ctrl+Alt (both :CTRL and :ALT
105
+ # present). Only emit the ESC Meta prefix when Alt is pressed WITHOUT
106
+ # Ctrl – i.e. a genuine Meta/Alt sequence, not an AltGr character.
107
+ alt_without_ctrl = key.control_keys.include?(:ALT) && !key.control_keys.include?(:CTRL)
108
+ @@output_buf.push("\e".ord) if alt_without_ctrl
109
+ # -------------------------------------------------------------------------
110
+
111
+ @@output_buf.concat(key.char.bytes)
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
@@ -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
@@ -192,7 +192,7 @@ class Chef
192
192
  # be overridden in subclasses.
193
193
  def managing_content?
194
194
  return true if new_resource.checksum
195
- return true if !new_resource.content.nil? && @action != :create_if_missing
195
+ return true if new_resource.property_is_set?(:content) && @action != :create_if_missing
196
196
 
197
197
  false
198
198
  end
@@ -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"
25
+ provides :ifconfig, platform_family: %w{fedora rhel amazon}
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()