summaryrefslogtreecommitdiff
path: root/lib/chef/provider/package/yum/yum_cache.rb
blob: fb25a91c8cd0a91ece76c7ade84763f780594fe6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376

# Author:: Adam Jacob (<adam@chef.io>)
# Copyright:: Copyright 2008-2016, Chef Software, Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

require "chef/config"
require "chef/provider/package"
require "chef/mixin/which"
require "chef/mixin/shell_out"
require "singleton"
require "chef/provider/package/yum/rpm_utils"

class Chef
  class Provider
    class Package
      class Yum < Chef::Provider::Package
        # Cache for our installed and available packages, pulled in from yum-dump.py
        class YumCache
          include Chef::Mixin::Which
          include Chef::Mixin::ShellOut
          include Singleton

          attr_accessor :yum_binary

          def initialize
            @rpmdb = RPMDb.new

            # Next time @rpmdb is accessed:
            #  :all       - Trigger a run of "yum-dump.py --options --installed-provides", updates
            #               yum's cache and parses options from /etc/yum.conf. Pulls in Provides
            #               dependency data for installed packages only - this data is slow to
            #               gather.
            #  :provides  - Same as :all but pulls in Provides data for available packages as well.
            #               Used as a last resort when we can't find a Provides match.
            #  :installed - Trigger a run of "yum-dump.py --installed", only reads the local rpm
            #               db. Used between client runs for a quick refresh.
            #  :none      - Do nothing, a call to one of the reload methods is required.
            @next_refresh = :all

            @allow_multi_install = []

            @extra_repo_control = nil

            # these are for subsequent runs if we are on an interval
            Chef::Client.when_run_starts do
              YumCache.instance.reload
            end
          end

          attr_reader :extra_repo_control

          # Cache management
          #

          def yum_dump_path
            ::File.join(::File.dirname(__FILE__), "yum-dump.py")
          end

          def refresh
            case @next_refresh
            when :none
              return nil
            when :installed
              reset_installed
              # fast
              opts = " --installed"
            when :all
              reset
              # medium
              opts = " --options --installed-provides"
            when :provides
              reset
              # slow!
              opts = " --options --all-provides"
            else
              raise ArgumentError, "Unexpected value in next_refresh: #{@next_refresh}"
            end

            if @extra_repo_control
              opts << " #{@extra_repo_control}"
            end

            opts << " --yum-lock-timeout #{Chef::Config[:yum_lock_timeout]}"

            one_line = false
            error = nil

            status = nil

            begin
              status = shell_out!("#{python_bin} #{yum_dump_path}#{opts}", :timeout => Chef::Config[:yum_timeout])
              status.stdout.each_line do |line|
                one_line = true

                line.chomp!
                if line =~ %r{\[option (.*)\] (.*)}
                  if $1 == "installonlypkgs"
                    @allow_multi_install = $2.split
                  else
                    raise Chef::Exceptions::Package, "Strange, unknown option line '#{line}' from yum-dump.py"
                  end
                  next
                end

                if line =~ %r{^(\S+) ([0-9]+) (\S+) (\S+) (\S+) \[(.*)\] ([i,a,r]) (\S+)$}
                  name     = $1
                  epoch    = $2
                  version  = $3
                  release  = $4
                  arch     = $5
                  provides = parse_provides($6)
                  type     = $7
                  repoid   = $8
                else
                  Chef::Log.warn("Problem parsing line '#{line}' from yum-dump.py! " +
                                 "Please check your yum configuration.")
                  next
                end

                case type
                when "i"
                  # if yum-dump was called with --installed this may not be true, but it's okay
                  # since we don't touch the @available Set in reload_installed
                  available = false
                  installed = true
                when "a"
                  available = true
                  installed = false
                when "r"
                  available = true
                  installed = true
                end

                pkg = RPMDbPackage.new(name, epoch, version, release, arch, provides, installed, available, repoid)
                @rpmdb << pkg
              end

              error = status.stderr
            rescue Mixlib::ShellOut::CommandTimeout => e
              Chef::Log.error("#{yum_dump_path} exceeded timeout #{Chef::Config[:yum_timeout]}")
              raise(e)
            end

            if status.exitstatus != 0
              raise Chef::Exceptions::Package, "Yum failed - #{status.inspect} - returns: #{error}"
            else
              unless one_line
                Chef::Log.warn("Odd, no output from yum-dump.py. Please check " +
                               "your yum configuration.")
              end
            end

            # A reload method must be called before the cache is altered
            @next_refresh = :none
          end

          def python_bin
            yum_executable = which(yum_binary)
            if yum_executable && shabang?(yum_executable)
              shabang_or_fallback(extract_interpreter(yum_executable))
            else
              Chef::Log.warn("Yum executable not found or doesn't start with #!. Using default python.")
              "/usr/bin/python"
            end
          rescue StandardError => e
            Chef::Log.warn("An error occurred attempting to determine correct python executable. Using default.")
            Chef::Log.debug(e)
            "/usr/bin/python"
          end

          def extract_interpreter(file)
            ::File.open(file, "r", &:readline)[2..-1].strip
          end

          # dnf based systems have a yum shim that has /bin/bash as the interpreter. Don't use this.
          def shabang_or_fallback(interpreter)
            if interpreter == "/bin/bash"
              Chef::Log.warn("Yum executable interpreter is /bin/bash. Falling back to default python.")
              "/usr/bin/python"
            else
              interpreter
            end
          end

          def shabang?(file)
            ::File.open(file, "r") do |f|
              f.read(2) == '#!'
            end
          rescue Errno::ENOENT
            false
          end

          def reload
            @next_refresh = :all
          end

          def reload_installed
            @next_refresh = :installed
          end

          def reload_provides
            @next_refresh = :provides
          end

          def reset
            @rpmdb.clear
          end

          def reset_installed
            @rpmdb.clear_installed
          end

          # Querying the cache
          #

          # Check for package by name or name+arch
          def package_available?(package_name)
            refresh

            if @rpmdb.lookup(package_name)
              return true
            else
              if package_name =~ %r{^(.*)\.(.*)$}
                pkg_name = $1
                pkg_arch = $2

                if matches = @rpmdb.lookup(pkg_name)
                  matches.each do |m|
                    return true if m.arch == pkg_arch
                  end
                end
              end
            end

            return false
          end

          # Returns a array of packages satisfying an RPMDependency
          def packages_from_require(rpmdep)
            refresh
            @rpmdb.whatprovides(rpmdep)
          end

          # Check if a package-version.arch is available to install
          def version_available?(package_name, desired_version, arch = nil)
            version(package_name, arch, true, false) do |v|
              return true if desired_version == v
            end

            return false
          end

          # Return the source repository for a package-version.arch
          def package_repository(package_name, desired_version, arch = nil)
            package(package_name, arch, true, false) do |pkg|
              return pkg.repoid if desired_version == pkg.version.to_s
            end

            return nil
          end

          # Return the latest available version for a package.arch
          def available_version(package_name, arch = nil)
            version(package_name, arch, true, false)
          end
          alias :candidate_version :available_version

          # Return the currently installed version for a package.arch
          def installed_version(package_name, arch = nil)
            version(package_name, arch, false, true)
          end

          # Return an array of packages allowed to be installed multiple times, such as the kernel
          def allow_multi_install
            refresh
            @allow_multi_install
          end

          def enable_extra_repo_control(arg)
            # Don't touch cache if it's the same repos as the last load
            unless @extra_repo_control == arg
              @extra_repo_control = arg
              reload
            end
          end

          def disable_extra_repo_control
            # Only force reload when set
            if @extra_repo_control
              @extra_repo_control = nil
              reload
            end
          end

          private

          def version(package_name, arch = nil, is_available = false, is_installed = false)
            package(package_name, arch, is_available, is_installed) do |pkg|
              if block_given?
                yield pkg.version.to_s
              else
                # first match is latest version
                return pkg.version.to_s
              end
            end

            if block_given?
              return self
            else
              return nil
            end
          end

          def package(package_name, arch = nil, is_available = false, is_installed = false)
            refresh
            packages = @rpmdb[package_name]
            if packages
              packages.each do |pkg|
                if is_available
                  next unless @rpmdb.available?(pkg)
                end
                if is_installed
                  next unless @rpmdb.installed?(pkg)
                end
                if arch
                  next unless pkg.arch == arch
                end

                if block_given?
                  yield pkg
                else
                  # first match is latest version
                  return pkg
                end
              end
            end

            if block_given?
              return self
            else
              return nil
            end
          end

          # Parse provides from yum-dump.py output
          def parse_provides(string)
            ret = []
            # ['atk = 1.12.2-1.fc6', 'libatk-1.0.so.0']
            string.split(", ").each do |seg|
              # 'atk = 1.12.2-1.fc6'
              if seg =~ %r{^'(.*)'$}
                ret << RPMProvide.parse($1)
              end
            end

            return ret
          end

        end # YumCache
      end
    end
  end
end