chef 0.10.2 → 0.10.4.rc.1

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 (73) hide show
  1. data/distro/common/html/chef-client.8.html +4 -4
  2. data/distro/common/html/knife-cookbook.1.html +5 -3
  3. data/distro/common/html/knife-node.1.html +4 -4
  4. data/distro/common/man/man1/knife-cookbook.1 +5 -1
  5. data/distro/common/man/man1/knife-node.1 +1 -1
  6. data/distro/common/markdown/man1/knife-cookbook-site.mkd +3 -3
  7. data/distro/common/markdown/man1/knife-cookbook.mkd +7 -0
  8. data/distro/common/markdown/man1/knife-node.mkd +4 -3
  9. data/distro/common/markdown/man1/knife-ssh.mkd +2 -0
  10. data/lib/chef/application.rb +1 -0
  11. data/lib/chef/cookbook_loader.rb +18 -0
  12. data/lib/chef/cookbook_uploader.rb +1 -1
  13. data/lib/chef/data_bag.rb +14 -2
  14. data/lib/chef/data_bag_item.rb +8 -2
  15. data/lib/chef/encrypted_data_bag_item.rb +19 -6
  16. data/lib/chef/environment.rb +12 -6
  17. data/lib/chef/exceptions.rb +1 -0
  18. data/lib/chef/knife.rb +0 -28
  19. data/lib/chef/knife/bootstrap.rb +7 -0
  20. data/lib/chef/knife/bootstrap/archlinux-gems.erb +14 -12
  21. data/lib/chef/knife/bootstrap/centos5-gems.erb +8 -5
  22. data/lib/chef/knife/bootstrap/fedora13-gems.erb +2 -0
  23. data/lib/chef/knife/bootstrap/ubuntu10.04-apt.erb +16 -9
  24. data/lib/chef/knife/bootstrap/ubuntu10.04-gems.erb +6 -3
  25. data/lib/chef/knife/client_bulk_delete.rb +28 -6
  26. data/lib/chef/knife/cookbook_site_install.rb +2 -2
  27. data/lib/chef/knife/cookbook_upload.rb +71 -0
  28. data/lib/chef/knife/core/bootstrap_context.rb +13 -3
  29. data/lib/chef/knife/core/cookbook_scm_repo.rb +2 -3
  30. data/lib/chef/knife/core/node_presenter.rb +5 -2
  31. data/lib/chef/knife/help.rb +13 -12
  32. data/lib/chef/knife/help_topics.rb +4 -0
  33. data/lib/chef/knife/ssh.rb +25 -4
  34. data/lib/chef/mixin/create_path.rb +3 -2
  35. data/lib/chef/mixin/get_source_from_package.rb +42 -0
  36. data/lib/chef/mixin/language.rb +8 -11
  37. data/lib/chef/monkey_patches/numeric.rb +9 -1
  38. data/lib/chef/monkey_patches/string.rb +21 -0
  39. data/lib/chef/platform.rb +2 -1
  40. data/lib/chef/provider.rb +1 -1
  41. data/lib/chef/provider/git.rb +16 -3
  42. data/lib/chef/provider/group/suse.rb +53 -0
  43. data/lib/chef/provider/mount/mount.rb +28 -20
  44. data/lib/chef/provider/package/apt.rb +39 -24
  45. data/lib/chef/provider/package/dpkg.rb +5 -2
  46. data/lib/chef/provider/package/easy_install.rb +2 -2
  47. data/lib/chef/provider/package/freebsd.rb +5 -2
  48. data/lib/chef/provider/package/macports.rb +4 -4
  49. data/lib/chef/provider/package/rpm.rb +4 -1
  50. data/lib/chef/provider/package/rubygems.rb +3 -0
  51. data/lib/chef/provider/package/solaris.rb +3 -0
  52. data/lib/chef/provider/package/yum-dump.py +239 -81
  53. data/lib/chef/provider/package/yum.rb +977 -110
  54. data/lib/chef/provider/package/zypper.rb +20 -3
  55. data/lib/chef/provider/remote_directory.rb +0 -1
  56. data/lib/chef/provider/service/arch.rb +35 -28
  57. data/lib/chef/provider/service/systemd.rb +102 -0
  58. data/lib/chef/provider/service/upstart.rb +8 -2
  59. data/lib/chef/providers.rb +2 -0
  60. data/lib/chef/resource.rb +31 -2
  61. data/lib/chef/resource/git.rb +9 -0
  62. data/lib/chef/resource/mount.rb +1 -2
  63. data/lib/chef/resource/yum_package.rb +20 -0
  64. data/lib/chef/rest.rb +1 -1
  65. data/lib/chef/role.rb +1 -1
  66. data/lib/chef/run_context.rb +3 -3
  67. data/lib/chef/runner.rb +15 -2
  68. data/lib/chef/shell_out.rb +1 -1
  69. data/lib/chef/shell_out/windows.rb +2 -2
  70. data/lib/chef/solr_query.rb +1 -1
  71. data/lib/chef/tasks/chef_repo.rake +1 -1
  72. data/lib/chef/version.rb +1 -1
  73. metadata +425 -441
@@ -6,9 +6,9 @@
6
6
  # Licensed under the Apache License, Version 2.0 (the "License");
7
7
  # you may not use this file except in compliance with the License.
8
8
  # You may obtain a copy of the License at
9
- #
9
+ #
10
10
  # http://www.apache.org/licenses/LICENSE-2.0
11
- #
11
+ #
12
12
  # Unless required by applicable law or agreed to in writing, software
13
13
  # distributed under the License is distributed on an "AS IS" BASIS,
14
14
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -20,154 +20,913 @@ require 'chef/provider/package'
20
20
  require 'chef/mixin/command'
21
21
  require 'chef/resource/package'
22
22
  require 'singleton'
23
+ require 'chef/mixin/get_source_from_package'
24
+
23
25
 
24
26
  class Chef
25
27
  class Provider
26
28
  class Package
27
29
  class Yum < Chef::Provider::Package
28
30
 
29
- class YumCache
30
- include Chef::Mixin::Command
31
- include Singleton
31
+ class RPMUtils
32
+ class << self
32
33
 
33
- def initialize
34
- load_data
34
+ # RPM::Version version_parse equivalent
35
+ def version_parse(evr)
36
+ return if evr.nil?
37
+
38
+ epoch = nil
39
+ # assume this is a version
40
+ version = evr
41
+ release = nil
42
+
43
+ lead = 0
44
+ tail = evr.size
45
+
46
+ if evr =~ %r{^([\d]+):}
47
+ epoch = $1.to_i
48
+ lead = $1.length + 1
49
+ elsif evr[0].ord == ":".ord
50
+ epoch = 0
51
+ lead = 1
52
+ end
53
+
54
+ if evr =~ %r{:?.*-(.*)$}
55
+ release = $1
56
+ tail = evr.length - release.length - lead - 1
57
+
58
+ if release.empty?
59
+ release = nil
60
+ end
61
+ end
62
+
63
+ version = evr[lead,tail]
64
+ if version.empty?
65
+ version = nil
66
+ end
67
+
68
+ [ epoch, version, release ]
69
+ end
70
+
71
+ # verify
72
+ def isalnum(x)
73
+ isalpha(x) or isdigit(x)
74
+ end
75
+
76
+ def isalpha(x)
77
+ v = x.ord
78
+ (v >= 65 and v <= 90) or (v >= 97 and v <= 122)
79
+ end
80
+
81
+ def isdigit(x)
82
+ v = x.ord
83
+ v >= 48 and v <= 57
84
+ end
85
+
86
+ # based on the reference spec in lib/rpmvercmp.c in rpm 4.9.0
87
+ def rpmvercmp(x, y)
88
+ # easy! :)
89
+ return 0 if x == y
90
+
91
+ if x.nil?
92
+ x = ""
93
+ end
94
+
95
+ if y.nil?
96
+ y = ""
97
+ end
98
+
99
+ # not so easy :(
100
+ #
101
+ # takes 2 strings like
102
+ #
103
+ # x = "1.20.b18.el5"
104
+ # y = "1.20.b17.el5"
105
+ #
106
+ # breaks into purely alpha and numeric segments and compares them using
107
+ # some rules
108
+ #
109
+ # * 10 > 1
110
+ # * 1 > a
111
+ # * z > a
112
+ # * Z > A
113
+ # * z > Z
114
+ # * leading zeros are ignored
115
+ # * separators (periods, commas) are ignored
116
+ # * "1.20.b18.el5.extrastuff" > "1.20.b18.el5"
117
+
118
+ x_pos = 0 # overall string element reference position
119
+ x_pos_max = x.length - 1 # number of elements in string, starting from 0
120
+ x_seg_pos = 0 # segment string element reference position
121
+ x_comp = nil # segment to compare
122
+
123
+ y_pos = 0
124
+ y_seg_pos = 0
125
+ y_pos_max = y.length - 1
126
+ y_comp = nil
127
+
128
+ while (x_pos <= x_pos_max and y_pos <= y_pos_max)
129
+ # first we skip over anything non alphanumeric
130
+ while (x_pos <= x_pos_max) and (isalnum(x[x_pos]) == false)
131
+ x_pos += 1 # +1 over pos_max if end of string
132
+ end
133
+ while (y_pos <= y_pos_max) and (isalnum(y[y_pos]) == false)
134
+ y_pos += 1
135
+ end
136
+
137
+ # if we hit the end of either we are done matching segments
138
+ if (x_pos == x_pos_max + 1) or (y_pos == y_pos_max + 1)
139
+ break
140
+ end
141
+
142
+ # we are now at the start of a alpha or numeric segment
143
+ x_seg_pos = x_pos
144
+ y_seg_pos = y_pos
145
+
146
+ # grab segment so we can compare them
147
+ if isdigit(x[x_seg_pos].ord)
148
+ x_seg_is_num = true
149
+
150
+ # already know it's a digit
151
+ x_seg_pos += 1
152
+
153
+ # gather up our digits
154
+ while (x_seg_pos <= x_pos_max) and isdigit(x[x_seg_pos])
155
+ x_seg_pos += 1
156
+ end
157
+ # copy the segment but not the unmatched character that x_seg_pos will
158
+ # refer to
159
+ x_comp = x[x_pos,x_seg_pos - x_pos]
160
+
161
+ while (y_seg_pos <= y_pos_max) and isdigit(y[y_seg_pos])
162
+ y_seg_pos += 1
163
+ end
164
+ y_comp = y[y_pos,y_seg_pos - y_pos]
165
+ else
166
+ # we are comparing strings
167
+ x_seg_is_num = false
168
+
169
+ while (x_seg_pos <= x_pos_max) and isalpha(x[x_seg_pos])
170
+ x_seg_pos += 1
171
+ end
172
+ x_comp = x[x_pos,x_seg_pos - x_pos]
173
+
174
+ while (y_seg_pos <= y_pos_max) and isalpha(y[y_seg_pos])
175
+ y_seg_pos += 1
176
+ end
177
+ y_comp = y[y_pos,y_seg_pos - y_pos]
178
+ end
179
+
180
+ # if y_seg_pos didn't advance in the above loop it means the segments are
181
+ # different types
182
+ if y_pos == y_seg_pos
183
+ # numbers always win over letters
184
+ return x_seg_is_num ? 1 : -1
185
+ end
186
+
187
+ # move the ball forward before we mess with the segments
188
+ x_pos += x_comp.length # +1 over pos_max if end of string
189
+ y_pos += y_comp.length
190
+
191
+ # we are comparing numbers - simply convert them
192
+ if x_seg_is_num
193
+ x_comp = x_comp.to_i
194
+ y_comp = y_comp.to_i
195
+ end
196
+
197
+ # compares ints or strings
198
+ # don't return if equal - try the next segment
199
+ if x_comp > y_comp
200
+ return 1
201
+ elsif x_comp < y_comp
202
+ return -1
203
+ end
204
+
205
+ # if we've reached here than the segments are the same - try again
206
+ end
207
+
208
+ # we must have reached the end of one or both of the strings and they
209
+ # matched up until this point
210
+
211
+ # segments matched completely but the segment separators were different -
212
+ # rpm reference code treats these as equal.
213
+ if (x_pos == x_pos_max + 1) and (y_pos == y_pos_max + 1)
214
+ return 0
215
+ end
216
+
217
+ # the most unprocessed characters left wins
218
+ if (x_pos_max - x_pos) > (y_pos_max - y_pos)
219
+ return 1
220
+ else
221
+ return -1
222
+ end
223
+ end
224
+
225
+ end # self
226
+ end # RPMUtils
227
+
228
+ class RPMVersion
229
+ include Comparable
230
+
231
+ def initialize(*args)
232
+ if args.size == 1
233
+ @e, @v, @r = RPMUtils.version_parse(args[0])
234
+ elsif args.size == 3
235
+ @e = args[0].to_i
236
+ @v = args[1]
237
+ @r = args[2]
238
+ else
239
+ raise ArgumentError, "Expecting either 'epoch-version-release' or 'epoch, " +
240
+ "version, release'"
241
+ end
242
+ end
243
+ attr_reader :e, :v, :r
244
+ alias :epoch :e
245
+ alias :version :v
246
+ alias :release :r
247
+
248
+ def self.parse(*args)
249
+ self.new(*args)
250
+ end
251
+
252
+ def <=>(y)
253
+ compare_versions(y)
254
+ end
255
+
256
+ def compare(y)
257
+ compare_versions(y, false)
258
+ end
259
+
260
+ def partial_compare(y)
261
+ compare_versions(y, true)
262
+ end
263
+
264
+ # RPM::Version rpm_version_to_s equivalent
265
+ def to_s
266
+ if @r.nil?
267
+ @v
268
+ else
269
+ "#{@v}-#{@r}"
270
+ end
35
271
  end
36
272
 
37
- def stale?
38
- interval = Chef::Config[:interval].to_f
273
+ def evr
274
+ "#{@e}:#{@v}-#{@r}"
275
+ end
276
+
277
+ private
278
+
279
+ # Rough RPM::Version rpm_version_cmp equivalent - except much slower :)
280
+ #
281
+ # partial lets epoch and version segment equality be good enough to return equal, eg:
282
+ #
283
+ # 2:1.2-1 == 2:1.2
284
+ # 2:1.2-1 == 2:
285
+ #
286
+ def compare_versions(y, partial=false)
287
+ x = self
288
+
289
+ # compare epoch
290
+ if (x.e.nil? == false and x.e > 0) and y.e.nil?
291
+ return 1
292
+ elsif x.e.nil? and (y.e.nil? == false and y.e > 0)
293
+ return -1
294
+ elsif x.e.nil? == false and y.e.nil? == false
295
+ if x.e < y.e
296
+ return -1
297
+ elsif x.e > y.e
298
+ return 1
299
+ end
300
+ end
301
+
302
+ # compare version
303
+ if partial and (x.v.nil? or y.v.nil?)
304
+ return 0
305
+ elsif x.v.nil? == false and y.v.nil?
306
+ return 1
307
+ elsif x.v.nil? and y.v.nil? == false
308
+ return -1
309
+ elsif x.v.nil? == false and y.v.nil? == false
310
+ cmp = RPMUtils.rpmvercmp(x.v, y.v)
311
+ return cmp if cmp != 0
312
+ end
313
+
314
+ # compare release
315
+ if partial and (x.r.nil? or y.r.nil?)
316
+ return 0
317
+ elsif x.r.nil? == false and y.r.nil?
318
+ return 1
319
+ elsif x.r.nil? and y.r.nil? == false
320
+ return -1
321
+ elsif x.r.nil? == false and y.r.nil? == false
322
+ cmp = RPMUtils.rpmvercmp(x.r, y.r)
323
+ return cmp
324
+ end
325
+
326
+ return 0
327
+ end
328
+ end
329
+
330
+ class RPMPackage
331
+ include Comparable
332
+
333
+ def initialize(*args)
334
+ if args.size == 4
335
+ @n = args[0]
336
+ @version = RPMVersion.new(args[1])
337
+ @a = args[2]
338
+ @provides = args[3]
339
+ elsif args.size == 6
340
+ @n = args[0]
341
+ e = args[1].to_i
342
+ v = args[2]
343
+ r = args[3]
344
+ @version = RPMVersion.new(e,v,r)
345
+ @a = args[4]
346
+ @provides = args[5]
347
+ else
348
+ raise ArgumentError, "Expecting either 'name, epoch-version-release, arch, provides' " +
349
+ "or 'name, epoch, version, release, arch, provides'"
350
+ end
351
+
352
+ # We always have one, ourselves!
353
+ if @provides.empty?
354
+ @provides = [ RPMProvide.new(@n, @version.evr, :==) ]
355
+ end
356
+ end
357
+ attr_reader :n, :a, :version, :provides
358
+ alias :name :n
359
+ alias :arch :a
360
+
361
+ def <=>(y)
362
+ compare(y)
363
+ end
364
+
365
+ def compare(y)
366
+ x = self
367
+
368
+ # easy! :)
369
+ return 0 if x.nevra == y.nevra
370
+
371
+ # compare name
372
+ if x.n.nil? == false and y.n.nil?
373
+ return 1
374
+ elsif x.n.nil? and y.n.nil? == false
375
+ return -1
376
+ elsif x.n.nil? == false and y.n.nil? == false
377
+ if x.n < y.n
378
+ return -1
379
+ elsif x.n > y.n
380
+ return 1
381
+ end
382
+ end
383
+
384
+ # compare version
385
+ if x.version > y.version
386
+ return 1
387
+ elsif x.version < y.version
388
+ return -1
389
+ end
390
+
391
+ # compare arch
392
+ if x.a.nil? == false and y.a.nil?
393
+ return 1
394
+ elsif x.a.nil? and y.a.nil? == false
395
+ return -1
396
+ elsif x.a.nil? == false and y.a.nil? == false
397
+ if x.a < y.a
398
+ return -1
399
+ elsif x.a > y.a
400
+ return 1
401
+ end
402
+ end
39
403
 
40
- # run once mode
41
- if interval == 0
404
+ return 0
405
+ end
406
+
407
+ def to_s
408
+ nevra
409
+ end
410
+
411
+ def nevra
412
+ "#{@n}-#{@version.evr}.#{@a}"
413
+ end
414
+ end
415
+
416
+ # Simple implementation from rpm and ruby-rpm reference code
417
+ class RPMDependency
418
+ def initialize(*args)
419
+ if args.size == 3
420
+ @name = args[0]
421
+ @version = RPMVersion.new(args[1])
422
+ # Our requirement to other dependencies
423
+ @flag = args[2] || :==
424
+ elsif args.size == 5
425
+ @name = args[0]
426
+ e = args[1].to_i
427
+ v = args[2]
428
+ r = args[3]
429
+ @version = RPMVersion.new(e,v,r)
430
+ @flag = args[4] || :==
431
+ else
432
+ raise ArgumentError, "Expecting either 'name, epoch-version-release, flag' or " +
433
+ "'name, epoch, version, release, flag'"
434
+ end
435
+ end
436
+ attr_reader :name, :version, :flag
437
+
438
+ # Parses 2 forms:
439
+ #
440
+ # "mtr >= 2:0.71-3.0"
441
+ # "mta"
442
+ def self.parse(string)
443
+ if string =~ %r{^(\S+)\s+(>|>=|=|==|<=|<)\s+(\S+)$}
444
+ name = $1
445
+ if $2 == "="
446
+ flag = :==
447
+ else
448
+ flag = :"#{$2}"
449
+ end
450
+ version = $3
451
+
452
+ return self.new(name, version, flag)
453
+ else
454
+ name = string
455
+ return self.new(name, nil, nil)
456
+ end
457
+ end
458
+
459
+ # Test if another RPMDependency satisfies our requirements
460
+ def satisfy?(y)
461
+ unless y.kind_of?(RPMDependency)
462
+ raise ArgumentError, "Expecting an RPMDependency object"
463
+ end
464
+
465
+ x = self
466
+
467
+ # Easy!
468
+ if x.name != y.name
42
469
  return false
43
- elsif (Time.now - @updated_at) > interval
470
+ end
471
+
472
+ # Partial compare
473
+ #
474
+ # eg: x.version 2.3 == y.version 2.3-1
475
+ sense = x.version.partial_compare(y.version)
476
+
477
+ # Thanks to rpmdsCompare() rpmds.c
478
+ if sense < 0 and (x.flag == :> || x.flag == :>=) || (y.flag == :<= || y.flag == :<)
479
+ return true
480
+ elsif sense > 0 and (x.flag == :< || x.flag == :<=) || (y.flag == :>= || y.flag == :>)
481
+ return true
482
+ elsif sense == 0 and (
483
+ ((x.flag == :== or x.flag == :<= or x.flag == :>=) and (y.flag == :== or y.flag == :<= or y.flag == :>=)) or
484
+ (x.flag == :< and y.flag == :<) or
485
+ (x.flag == :> and y.flag == :>)
486
+ )
44
487
  return true
45
488
  end
46
489
 
47
- false
490
+ return false
48
491
  end
49
-
50
- def refresh
51
- if @data.empty?
52
- reload
53
- elsif stale?
54
- reload
492
+ end
493
+
494
+ class RPMProvide < RPMDependency; end
495
+ class RPMRequire < RPMDependency; end
496
+
497
+ class RPMDbPackage < RPMPackage
498
+ # <rpm parts>, installed, available
499
+ def initialize(*args)
500
+ # state
501
+ @available = args.pop
502
+ @installed = args.pop
503
+ super(*args)
504
+ end
505
+ attr_reader :available, :installed
506
+ end
507
+
508
+ # Simple storage for RPMPackage objects - keeps them unique and sorted
509
+ class RPMDb
510
+ def initialize
511
+ # package name => [ RPMPackage, RPMPackage ] of different versions
512
+ @rpms = Hash.new
513
+ # package nevra => RPMPackage for lookups
514
+ @index = Hash.new
515
+ # provide name (aka feature) => [RPMPackage, RPMPackage] each providing this feature
516
+ @provides = Hash.new
517
+ # RPMPackages listed as available
518
+ @available = Set.new
519
+ # RPMPackages listed as installed
520
+ @installed = Set.new
521
+ end
522
+
523
+ def [](package_name)
524
+ self.lookup(package_name)
525
+ end
526
+
527
+ # Lookup package_name and return a descending array of package objects
528
+ def lookup(package_name)
529
+ pkgs = @rpms[package_name]
530
+ if pkgs
531
+ return pkgs.sort.reverse
532
+ else
533
+ return nil
55
534
  end
56
535
  end
57
536
 
58
- def load_data
59
- @data = Hash.new
60
- error = String.new
537
+ def lookup_provides(provide_name)
538
+ @provides[provide_name]
539
+ end
540
+
541
+ # Using the package name as a key, and nevra for an index, keep a unique list of packages.
542
+ # The available/installed state can be overwritten for existing packages.
543
+ def push(*args)
544
+ args.flatten.each do |new_rpm|
545
+ unless new_rpm.kind_of?(RPMDbPackage)
546
+ raise ArgumentError, "Expecting an RPMDbPackage object"
547
+ end
61
548
 
62
- helper = ::File.join(::File.dirname(__FILE__), 'yum-dump.py')
63
- status = popen4("python #{helper}", :waitlast => true) do |pid, stdin, stdout, stderr|
64
- stdout.each do |line|
65
- line.chomp!
66
- name, type, epoch, version, release, arch = line.split(',')
67
- type_sym = type.to_sym
68
- if !@data.has_key?(name)
69
- @data[name] = Hash.new
70
- end
71
- if !@data[name].has_key?(type_sym)
72
- @data[name][type_sym] = Hash.new
549
+ @rpms[new_rpm.n] ||= Array.new
550
+
551
+ # we may already have this one, like when the installed list is refreshed
552
+ idx = @index[new_rpm.nevra]
553
+ if idx
554
+ # grab the existing package if it's not
555
+ curr_rpm = idx
556
+ else
557
+ @rpms[new_rpm.n] << new_rpm
558
+
559
+ new_rpm.provides.each do |provide|
560
+ @provides[provide.name] ||= Array.new
561
+ @provides[provide.name] << new_rpm
73
562
  end
74
- @data[name][type_sym][arch] = { :epoch => epoch, :version => version,
75
- :release => release }
563
+
564
+ curr_rpm = new_rpm
76
565
  end
77
-
78
- error = stderr.readlines
79
- end
80
566
 
81
- unless status.exitstatus == 0
82
- raise Chef::Exceptions::Package, "yum failed - #{status.inspect} - returns: #{error}"
567
+ # Track the nevra -> RPMPackage association to avoid having to compare versions
568
+ # with @rpms[new_rpm.n] on the next round
569
+ @index[new_rpm.nevra] = curr_rpm
570
+
571
+ # these are overwritten for existing packages
572
+ if new_rpm.available
573
+ @available << curr_rpm
574
+ end
575
+ if new_rpm.installed
576
+ @installed << curr_rpm
577
+ end
83
578
  end
579
+ end
84
580
 
85
- @updated_at = Time.now
581
+ def <<(*args)
582
+ self.push(args)
86
583
  end
87
- alias :reload :load_data
88
584
 
89
- def version(package_name, type, arch)
90
- if (x = @data[package_name])
91
- if (y = x[type])
92
- if arch
93
- if (z = y[arch])
94
- return "#{z[:version]}-#{z[:release]}"
585
+ def clear
586
+ @rpms.clear
587
+ @index.clear
588
+ @provides.clear
589
+ clear_available
590
+ clear_installed
591
+ end
592
+
593
+ def clear_available
594
+ @available.clear
595
+ end
596
+
597
+ def clear_installed
598
+ @installed.clear
599
+ end
600
+
601
+ def size
602
+ @rpms.size
603
+ end
604
+ alias :length :size
605
+
606
+ def available_size
607
+ @available.size
608
+ end
609
+
610
+ def installed_size
611
+ @installed.size
612
+ end
613
+
614
+ def available?(package)
615
+ @available.include?(package)
616
+ end
617
+
618
+ def installed?(package)
619
+ @installed.include?(package)
620
+ end
621
+
622
+ def whatprovides(rpmdep)
623
+ unless rpmdep.kind_of?(RPMDependency)
624
+ raise ArgumentError, "Expecting an RPMDependency object"
625
+ end
626
+
627
+ what = []
628
+
629
+ packages = lookup_provides(rpmdep.name)
630
+ if packages
631
+ packages.each do |pkg|
632
+ pkg.provides.each do |provide|
633
+ if provide.satisfy?(rpmdep)
634
+ what << pkg
95
635
  end
96
- else
97
- # no arch specified - take the first match
98
- z = y.to_a[0][1]
99
- return "#{z[:version]}-#{z[:release]}"
100
636
  end
101
637
  end
102
638
  end
103
639
 
104
- nil
640
+ return what
105
641
  end
642
+ end
106
643
 
107
- def version_available?(package_name, desired_version, arch)
108
- if (package_data = @data[package_name])
109
- if (available_versions = package_data[:available])
110
- if arch
111
- # arch gets passed like ".x86_64"
112
- matching_versions = [ available_versions[arch.sub(/^./, '')]]
113
- else
114
- matching_versions = available_versions.values
115
- end
644
+ # Cache for our installed and available packages, pulled in from yum-dump.py
645
+ class YumCache
646
+ include Chef::Mixin::Command
647
+ include Singleton
648
+
649
+ def initialize
650
+ @rpmdb = RPMDb.new
651
+
652
+ # Next time @rpmdb is accessed:
653
+ # :all - Trigger a run of "yum-dump.py --options --installed-provides", updates
654
+ # yum's cache and parses options from /etc/yum.conf. Pulls in Provides
655
+ # dependency data for installed packages only - this data is slow to
656
+ # gather.
657
+ # :provides - Same as :all but pulls in Provides data for available packages as well.
658
+ # Used as a last resort when we can't find a Provides match.
659
+ # :installed - Trigger a run of "yum-dump.py --installed", only reads the local rpm
660
+ # db. Used between client runs for a quick refresh.
661
+ # :none - Do nothing, a call to one of the reload methods is required.
662
+ @next_refresh = :all
663
+
664
+ @allow_multi_install = []
665
+
666
+ # these are for subsequent runs if we are on an interval
667
+ Chef::Client.when_run_starts do
668
+ YumCache.instance.reload
669
+ end
670
+ end
671
+
672
+ # Cache management
673
+ #
116
674
 
117
- if matching_versions.nil?
118
- if arch.empty?
119
- arch_msg = ""
675
+ def refresh
676
+ case @next_refresh
677
+ when :none
678
+ return nil
679
+ when :installed
680
+ reset_installed
681
+ # fast
682
+ opts=" --installed"
683
+ when :all
684
+ reset
685
+ # medium
686
+ opts=" --options --installed-provides"
687
+ when :provides
688
+ reset
689
+ # slow!
690
+ opts=" --options --all-provides"
691
+ else
692
+ raise ArgumentError, "Unexpected value in next_refresh: #{@next_refresh}"
693
+ end
694
+
695
+ one_line = false
696
+ error = nil
697
+
698
+ helper = ::File.join(::File.dirname(__FILE__), 'yum-dump.py')
699
+
700
+ status = popen4("/usr/bin/python #{helper}#{opts}", :waitlast => true) do |pid, stdin, stdout, stderr|
701
+ stdout.each do |line|
702
+ one_line = true
703
+
704
+ line.chomp!
705
+
706
+ if line =~ %r{\[option (.*)\] (.*)}
707
+ if $1 == "installonlypkgs"
708
+ @allow_multi_install = $2.split
120
709
  else
121
- arch_msg = "with arch #{arch.sub(/^./, '')} "
710
+ raise Chef::Exceptions::Package, "Strange, unknown option line '#{line}' from yum-dump.py"
122
711
  end
712
+ next
713
+ end
123
714
 
124
- raise ArgumentError, "#{package_name}: Found no available versions #{arch_msg}to match"
715
+ if line =~ %r{^(\S+) ([0-9]+) (\S+) (\S+) (\S+) \[(.*)\] ([i,a,r])$}
716
+ name = $1
717
+ epoch = $2
718
+ version = $3
719
+ release = $4
720
+ arch = $5
721
+ provides = parse_provides($6)
722
+ type = $7
723
+ else
724
+ Chef::Log.warn("Problem parsing line '#{line}' from yum-dump.py! " +
725
+ "Please check your yum configuration.")
726
+ next
125
727
  end
126
728
 
127
- # Expect [ { :version => "ver", :release => "rel" }, { :version => "ver", :release => "rel" }, { :version => "ver", :release => "rel" } ] ???
128
- matching_versions.each do |ver|
129
- Chef::Log.debug("#{@new_resource} trying to match #{desired_version} to version #{ver[:version]} and release #{ver[:release]}")
130
- if (desired_version == "#{ver[:version]}-#{ver[:release]}")
131
- return true
132
- end
729
+ case type
730
+ when "i"
731
+ # if yum-dump was called with --installed this may not be true, but it's okay
732
+ # since we don't touch the @available Set in reload_installed
733
+ available = false
734
+ installed = true
735
+ when "a"
736
+ available = true
737
+ installed = false
738
+ when "r"
739
+ available = true
740
+ installed = true
133
741
  end
742
+
743
+ pkg = RPMDbPackage.new(name, epoch, version, release, arch, provides, installed, available)
744
+ @rpmdb << pkg
134
745
  end
746
+
747
+ error = stderr.readlines
135
748
  end
136
749
 
137
- nil
750
+ if status.exitstatus != 0
751
+ raise Chef::Exceptions::Package, "Yum failed - #{status.inspect} - returns: #{error}"
752
+ else
753
+ unless one_line
754
+ Chef::Log.warn("Odd, no output from yum-dump.py. Please check " +
755
+ "your yum configuration.")
756
+ end
757
+ end
758
+
759
+ # A reload method must be called before the cache is altered
760
+ @next_refresh = :none
138
761
  end
139
762
 
140
- def installed_version(package_name, arch)
141
- version(package_name, :installed, arch)
763
+ def reload
764
+ @next_refresh = :all
142
765
  end
143
766
 
144
- def candidate_version(package_name, arch)
145
- version(package_name, :available, arch)
767
+ def reload_installed
768
+ @next_refresh = :installed
146
769
  end
147
770
 
148
- def flush
149
- @data.clear
771
+ def reload_provides
772
+ @next_refresh = :provides
773
+ end
774
+
775
+ def reset
776
+ @rpmdb.clear
777
+ end
778
+
779
+ def reset_installed
780
+ @rpmdb.clear_installed
781
+ end
782
+
783
+ # Querying the cache
784
+ #
785
+
786
+ def package_available?(package_name)
787
+ refresh
788
+ if @rpmdb.lookup(package_name)
789
+ true
790
+ else
791
+ false
792
+ end
793
+ end
794
+
795
+ # Returns a array of packages satisfying an RPMDependency
796
+ def packages_from_require(rpmdep)
797
+ refresh
798
+ @rpmdb.whatprovides(rpmdep)
799
+ end
800
+
801
+ def version_available?(package_name, desired_version, arch=nil)
802
+ version(package_name, arch, true, false) do |v|
803
+ return true if desired_version == v
804
+ end
805
+
806
+ return false
807
+ end
808
+
809
+ def available_version(package_name, arch=nil)
810
+ version(package_name, arch, true, false)
811
+ end
812
+ alias :candidate_version :available_version
813
+
814
+ def installed_version(package_name, arch=nil)
815
+ version(package_name, arch, false, true)
150
816
  end
151
- end
817
+
818
+ def allow_multi_install
819
+ refresh
820
+ @allow_multi_install
821
+ end
822
+
823
+ private
824
+ def version(package_name, arch=nil, is_available=false, is_installed=false)
825
+ refresh
826
+ packages = @rpmdb[package_name]
827
+ if packages
828
+ packages.each do |pkg|
829
+ if is_available
830
+ next unless @rpmdb.available?(pkg)
831
+ end
832
+ if is_installed
833
+ next unless @rpmdb.installed?(pkg)
834
+ end
835
+ if arch
836
+ next unless pkg.arch == arch
837
+ end
838
+
839
+ if block_given?
840
+ yield pkg.version.to_s
841
+ else
842
+ # first match is latest version
843
+ return pkg.version.to_s
844
+ end
845
+ end
846
+ end
847
+
848
+ if block_given?
849
+ return self
850
+ else
851
+ return nil
852
+ end
853
+ end
854
+
855
+ # Parse provides from yum-dump.py output
856
+ def parse_provides(string)
857
+ ret = []
858
+ # ['atk = 1.12.2-1.fc6', 'libatk-1.0.so.0']
859
+ string.split(", ").each do |seg|
860
+ # 'atk = 1.12.2-1.fc6'
861
+ if seg =~ %r{^'(.*)'$}
862
+ ret << RPMProvide.parse($1)
863
+ end
864
+ end
865
+
866
+ return ret
867
+ end
868
+
869
+ end # YumCache
870
+
871
+ include Chef::Mixin::GetSourceFromPackage
152
872
 
153
873
  def initialize(new_resource, run_context)
154
874
  super
875
+
155
876
  @yum = YumCache.instance
156
877
  end
157
878
 
879
+ # Extra attributes
880
+ #
881
+
158
882
  def arch
159
883
  if @new_resource.respond_to?("arch")
160
- @new_resource.arch
884
+ @new_resource.arch
161
885
  else
162
886
  nil
163
887
  end
164
888
  end
165
889
 
890
+ def flush_cache
891
+ if @new_resource.respond_to?("flush_cache")
892
+ @new_resource.flush_cache
893
+ else
894
+ { :before => false, :after => false }
895
+ end
896
+ end
897
+
898
+ def allow_downgrade
899
+ if @new_resource.respond_to?("allow_downgrade")
900
+ @new_resource.allow_downgrade
901
+ else
902
+ false
903
+ end
904
+ end
905
+
906
+ # Helpers
907
+ #
908
+
166
909
  def yum_arch
167
910
  arch ? ".#{arch}" : nil
168
911
  end
169
912
 
913
+ # Standard Provider methods for Parent
914
+ #
915
+
170
916
  def load_current_resource
917
+ if flush_cache[:before]
918
+ @yum.reload
919
+ end
920
+
921
+ unless @yum.package_available?(@new_resource.package_name)
922
+ parse_dependency
923
+ end
924
+
925
+ # Don't overwrite an existing arch
926
+ unless arch
927
+ parse_arch
928
+ end
929
+
171
930
  @current_resource = Chef::Resource::Package.new(@new_resource.name)
172
931
  @current_resource.package_name(@new_resource.package_name)
173
932
 
@@ -188,73 +947,181 @@ class Chef
188
947
  end
189
948
  end
190
949
 
191
- Chef::Log.debug("#{@new_resource} checking yum info for #{@new_resource.package_name}#{yum_arch}")
950
+ if @new_resource.version
951
+ new_resource = "#{@new_resource.package_name}-#{@new_resource.version}#{yum_arch}"
952
+ else
953
+ new_resource = "#{@new_resource.package_name}#{yum_arch}"
954
+ end
192
955
 
193
- @yum.refresh
956
+ Chef::Log.debug("#{@new_resource} checking yum info for #{new_resource}")
194
957
 
195
958
  installed_version = @yum.installed_version(@new_resource.package_name, arch)
959
+ @current_resource.version(installed_version)
960
+
196
961
  @candidate_version = @yum.candidate_version(@new_resource.package_name, arch)
197
962
 
198
- @current_resource.version(installed_version)
199
- if candidate_version
200
- @candidate_version = candidate_version
201
- else
202
- @candidate_version = installed_version
203
- end
204
- Chef::Log.debug("#{@new_resource} installed version: #{installed_version} candidate version: #{candidate_version}")
963
+ Chef::Log.debug("#{@new_resource} installed version: #{installed_version || "(none)"} candidate version: " +
964
+ "#{@candidate_version || "(none)"}")
205
965
 
206
966
  @current_resource
207
967
  end
208
968
 
209
969
  def install_package(name, version)
210
- if @new_resource.source
970
+ if @new_resource.source
211
971
  run_command_with_systems_locale(
212
- :command => "yum -d0 -e0 -y #{@new_resource.options} localinstall #{@new_resource.source}"
972
+ :command => "yum -d0 -e0 -y#{expand_options(@new_resource.options)} localinstall #{@new_resource.source}"
213
973
  )
214
974
  else
215
975
  # Work around yum not exiting with an error if a package doesn't exist for CHEF-2062
216
- if @yum.version_available?(name, version, yum_arch)
976
+ if @yum.version_available?(name, version, arch)
977
+ method = "install"
978
+
979
+ # More Yum fun:
980
+ #
981
+ # yum install of an old name+version will exit(1)
982
+ # yum install of an old name+version+arch will exit(0) for some reason
983
+ #
984
+ # Some packages can be installed multiple times like the kernel
985
+ unless @yum.allow_multi_install.include?(name)
986
+ if RPMVersion.parse(@current_resource.version) > RPMVersion.parse(version)
987
+ # Unless they want this...
988
+ if allow_downgrade
989
+ method = "downgrade"
990
+ else
991
+ # we bail like yum when the package is older
992
+ raise Chef::Exceptions::Package, "Installed package #{name}-#{@current_resource.version} is newer " +
993
+ "than candidate package #{name}-#{version}"
994
+ end
995
+ end
996
+ end
997
+
217
998
  run_command_with_systems_locale(
218
- :command => "yum -d0 -e0 -y #{@new_resource.options} install #{name}-#{version}#{yum_arch}"
999
+ :command => "yum -d0 -e0 -y#{expand_options(@new_resource.options)} #{method} #{name}-#{version}#{yum_arch}"
219
1000
  )
220
1001
  else
221
- raise ArgumentError, "#{@new_resource.name}: Version #{version} of #{name} not found. Did you specify both version and release? (version-release, e.g. 1.84-10.fc6)"
1002
+ raise Chef::Exceptions::Package, "Version #{version} of #{name} not found. Did you specify both version " +
1003
+ "and release? (version-release, e.g. 1.84-10.fc6)"
222
1004
  end
223
1005
  end
224
- @yum.flush
1006
+ if flush_cache[:after]
1007
+ @yum.reload
1008
+ else
1009
+ @yum.reload_installed
1010
+ end
225
1011
  end
226
1012
 
227
- def upgrade_package(name, version)
228
- # If we're not given a version, running update is the correct
229
- # option. If we are, then running install_package is right.
230
- unless version
231
- run_command_with_systems_locale(
232
- :command => "yum -d0 -e0 -y #{@new_resource.options} update #{name}#{yum_arch}"
233
- )
234
- @yum.flush
1013
+ # Keep upgrades from trying to install an older candidate version. Can happen when a new
1014
+ # version is installed then removed from a repository, now the older available version
1015
+ # shows up as a viable install candidate.
1016
+ #
1017
+ # Can be done in upgrade_package but an upgraded from->to log message slips out
1018
+ #
1019
+ # Hacky - better overall solution? Custom compare in Package provider?
1020
+ def action_upgrade
1021
+ # Ensure the candidate is newer
1022
+ if RPMVersion.parse(candidate_version) > RPMVersion.parse(@current_resource.version)
1023
+ super
1024
+ # Candidate is older
235
1025
  else
236
- install_package(name, version)
1026
+ Chef::Log.debug("#{@new_resource} is at the latest version - nothing to do")
237
1027
  end
238
1028
  end
239
1029
 
1030
+ def upgrade_package(name, version)
1031
+ install_package(name, version)
1032
+ end
1033
+
240
1034
  def remove_package(name, version)
241
1035
  if version
242
1036
  run_command_with_systems_locale(
243
- :command => "yum -d0 -e0 -y #{@new_resource.options} remove #{name}-#{version}#{yum_arch}"
1037
+ :command => "yum -d0 -e0 -y#{expand_options(@new_resource.options)} remove #{name}-#{version}#{yum_arch}"
244
1038
  )
245
1039
  else
246
1040
  run_command_with_systems_locale(
247
- :command => "yum -d0 -e0 -y #{@new_resource.options} remove #{name}#{yum_arch}"
1041
+ :command => "yum -d0 -e0 -y#{expand_options(@new_resource.options)} remove #{name}#{yum_arch}"
248
1042
  )
249
1043
  end
250
-
251
- @yum.flush
1044
+ if flush_cache[:after]
1045
+ @yum.reload
1046
+ else
1047
+ @yum.reload_installed
1048
+ end
252
1049
  end
253
1050
 
254
1051
  def purge_package(name, version)
255
1052
  remove_package(name, version)
256
1053
  end
257
1054
 
1055
+ private
1056
+
1057
+ def parse_arch
1058
+ # Allow for foo.x86_64 style package_name like yum uses in it's output
1059
+ #
1060
+ if @new_resource.package_name =~ %r{^(.*)\.(.*)$}
1061
+ new_package_name = $1
1062
+ new_arch = $2
1063
+ # foo.i386 and foo.beta1 are both valid package names or expressions of an arch.
1064
+ # Ensure we don't have an existing package matching package_name, then ensure we at
1065
+ # least have a match for the new_package+new_arch before we overwrite. If neither
1066
+ # then fall through to standard package handling.
1067
+ if (@yum.installed_version(@new_resource.package_name).nil? and @yum.candidate_version(@new_resource.package_name).nil?) and
1068
+ (@yum.installed_version(new_package_name, new_arch) or @yum.candidate_version(new_package_name, new_arch))
1069
+ @new_resource.package_name(new_package_name)
1070
+ @new_resource.arch(new_arch)
1071
+ end
1072
+ end
1073
+ end
1074
+
1075
+ # If we don't have the package we could have been passed a 'whatprovides' feature
1076
+ #
1077
+ # eg: yum install "perl(Config)"
1078
+ # yum install "mtr = 2:0.71-3.1"
1079
+ # yum install "mtr > 2:0.71"
1080
+ #
1081
+ # We support resolving these out of the Provides data imported from yum-dump.py and
1082
+ # matching them up with an actual package so the standard resource handling can apply.
1083
+ #
1084
+ # There is currently no support for filename matching.
1085
+ def parse_dependency
1086
+ # Transform the package_name into a requirement
1087
+ yum_require = RPMRequire.parse(@new_resource.package_name)
1088
+ # and gather all the packages that have a Provides feature satisfying the requirement.
1089
+ # It could be multiple be we can only manage one
1090
+ packages = @yum.packages_from_require(yum_require)
1091
+
1092
+ if packages.empty?
1093
+ # Don't bother if we are just ensuring a package is removed - we don't need Provides data
1094
+ actions = Array(@new_resource.action)
1095
+ unless actions.size == 1 and (actions[0] == :remove || actions[0] == :purge)
1096
+ Chef::Log.debug("#{@new_resource} couldn't match #{@new_resource.package_name} in " +
1097
+ "installed Provides, loading available Provides - this may take a moment")
1098
+ @yum.reload_provides
1099
+ packages = @yum.packages_from_require(yum_require)
1100
+ end
1101
+ end
1102
+
1103
+ unless packages.empty?
1104
+ new_package_name = packages.first.name
1105
+ Chef::Log.debug("#{@new_resource} no package found for #{@new_resource.package_name} " +
1106
+ "but matched Provides for #{new_package_name}")
1107
+
1108
+ # Ensure it's not the same package under a different architecture
1109
+ unique_names = []
1110
+ packages.each do |pkg|
1111
+ unique_names << "#{pkg.name}-#{pkg.version.evr}"
1112
+ end
1113
+ unique_names.uniq!
1114
+
1115
+ if unique_names.size > 1
1116
+ Chef::Log.warn("#{@new_resource} matched multiple Provides for #{@new_resource.package_name} " +
1117
+ "but we can only use the first match: #{new_package_name}. Please use a more " +
1118
+ "specific version.")
1119
+ end
1120
+
1121
+ @new_resource.package_name(new_package_name)
1122
+ end
1123
+ end
1124
+
258
1125
  end
259
1126
  end
260
1127
  end