chef 0.10.2 → 0.10.4.rc.1

Sign up to get free protection for your applications and to get access to all the features.
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