chef 0.7.16 → 0.8.2

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of chef might be problematic. Click here for more details.

Files changed (180) hide show
  1. data/README.rdoc +11 -10
  2. data/bin/chef-client +2 -2
  3. data/bin/chef-solo +1 -1
  4. data/bin/knife +27 -0
  5. data/bin/shef +49 -0
  6. data/distro/README +2 -0
  7. data/distro/{debian → common}/man/man1/chef-indexer.1 +0 -0
  8. data/distro/{debian → common}/man/man1/chef-server.1 +0 -0
  9. data/distro/{debian → common}/man/man8/chef-client.8 +0 -0
  10. data/distro/{debian → common}/man/man8/chef-solo.8 +0 -0
  11. data/distro/common/man/man8/knife.8 +375 -0
  12. data/distro/redhat/etc/init.d/chef-client +8 -4
  13. data/distro/redhat/etc/init.d/chef-server +16 -15
  14. data/distro/redhat/etc/init.d/chef-server-webui +78 -0
  15. data/distro/redhat/etc/init.d/chef-solr +76 -0
  16. data/distro/redhat/etc/init.d/chef-solr-indexer +75 -0
  17. data/distro/redhat/etc/sysconfig/chef-client +10 -0
  18. data/distro/redhat/etc/sysconfig/chef-server +10 -0
  19. data/distro/redhat/etc/sysconfig/chef-server-webui +10 -0
  20. data/distro/redhat/etc/sysconfig/chef-solr +9 -0
  21. data/distro/redhat/etc/sysconfig/chef-solr-indexer +7 -0
  22. data/distro/suse/etc/init.d/chef-client +121 -0
  23. data/lib/chef.rb +1 -1
  24. data/lib/chef/api_client.rb +263 -0
  25. data/lib/chef/application.rb +1 -1
  26. data/lib/chef/application/client.rb +21 -3
  27. data/lib/chef/application/knife.rb +144 -0
  28. data/lib/chef/application/server.rb +2 -1
  29. data/lib/chef/application/solo.rb +9 -2
  30. data/lib/chef/cache.rb +61 -0
  31. data/lib/chef/cache/checksum.rb +70 -0
  32. data/lib/chef/certificate.rb +154 -0
  33. data/lib/chef/client.rb +123 -220
  34. data/lib/chef/compile.rb +9 -21
  35. data/lib/chef/config.rb +67 -10
  36. data/lib/chef/cookbook.rb +49 -22
  37. data/lib/chef/cookbook/metadata.rb +85 -5
  38. data/lib/chef/cookbook_loader.rb +4 -4
  39. data/lib/chef/couchdb.rb +99 -30
  40. data/lib/chef/daemon.rb +1 -1
  41. data/lib/chef/data_bag.rb +215 -0
  42. data/lib/chef/data_bag_item.rb +219 -0
  43. data/lib/chef/exceptions.rb +3 -0
  44. data/lib/chef/index_queue.rb +29 -0
  45. data/lib/chef/index_queue/amqp_client.rb +106 -0
  46. data/lib/chef/index_queue/consumer.rb +76 -0
  47. data/lib/chef/index_queue/indexable.rb +74 -0
  48. data/lib/chef/knife.rb +309 -0
  49. data/lib/chef/knife/client_bulk_delete.rb +40 -0
  50. data/lib/chef/knife/client_create.rb +62 -0
  51. data/lib/chef/knife/client_delete.rb +37 -0
  52. data/lib/chef/knife/client_edit.rb +37 -0
  53. data/lib/chef/knife/client_list.rb +40 -0
  54. data/lib/chef/knife/client_reregister.rb +48 -0
  55. data/lib/chef/knife/client_show.rb +42 -0
  56. data/lib/chef/knife/configure.rb +123 -0
  57. data/lib/chef/knife/cookbook_bulk_delete.rb +46 -0
  58. data/lib/chef/knife/cookbook_delete.rb +41 -0
  59. data/lib/chef/knife/cookbook_download.rb +57 -0
  60. data/lib/chef/knife/cookbook_list.rb +41 -0
  61. data/lib/chef/knife/cookbook_metadata.rb +87 -0
  62. data/lib/chef/knife/cookbook_show.rb +75 -0
  63. data/lib/chef/knife/cookbook_upload.rb +179 -0
  64. data/lib/chef/knife/data_bag_create.rb +43 -0
  65. data/lib/chef/knife/data_bag_delete.rb +43 -0
  66. data/lib/chef/knife/data_bag_edit.rb +49 -0
  67. data/lib/chef/knife/data_bag_list.rb +42 -0
  68. data/lib/chef/knife/data_bag_show.rb +40 -0
  69. data/lib/chef/knife/ec2_instance_data.rb +46 -0
  70. data/lib/chef/knife/index_rebuild.rb +51 -0
  71. data/lib/chef/knife/node_bulk_delete.rb +43 -0
  72. data/lib/chef/knife/node_create.rb +39 -0
  73. data/lib/chef/knife/node_delete.rb +36 -0
  74. data/lib/chef/knife/node_edit.rb +36 -0
  75. data/lib/chef/knife/node_from_file.rb +42 -0
  76. data/lib/chef/knife/node_list.rb +41 -0
  77. data/lib/chef/knife/node_run_list_add.rb +64 -0
  78. data/lib/chef/knife/node_run_list_remove.rb +45 -0
  79. data/lib/chef/knife/node_show.rb +46 -0
  80. data/lib/chef/knife/role_bulk_delete.rb +44 -0
  81. data/lib/chef/knife/role_create.rb +44 -0
  82. data/lib/chef/knife/role_delete.rb +36 -0
  83. data/lib/chef/knife/role_edit.rb +37 -0
  84. data/lib/chef/knife/role_from_file.rb +46 -0
  85. data/lib/chef/knife/role_list.rb +40 -0
  86. data/lib/chef/knife/role_show.rb +43 -0
  87. data/lib/chef/knife/search.rb +94 -0
  88. data/lib/chef/knife/ssh.rb +170 -0
  89. data/lib/chef/log.rb +30 -8
  90. data/lib/chef/mixin/checksum.rb +2 -7
  91. data/lib/chef/mixin/command.rb +32 -13
  92. data/lib/chef/mixin/convert_to_class_name.rb +15 -0
  93. data/lib/chef/mixin/deep_merge.rb +199 -11
  94. data/lib/chef/mixin/generate_url.rb +18 -9
  95. data/lib/chef/mixin/language.rb +29 -1
  96. data/lib/chef/mixin/language_include_attribute.rb +56 -0
  97. data/lib/chef/mixin/language_include_recipe.rb +53 -0
  98. data/lib/chef/mixin/params_validate.rb +25 -12
  99. data/lib/chef/mixin/recipe_definition_dsl_core.rb +2 -0
  100. data/lib/chef/mixin/template.rb +11 -1
  101. data/lib/chef/mixin/xml_escape.rb +87 -0
  102. data/lib/chef/node.rb +144 -122
  103. data/lib/chef/openid_registration.rb +12 -5
  104. data/lib/chef/platform.rb +89 -47
  105. data/lib/chef/provider/breakpoint.rb +36 -0
  106. data/lib/chef/provider/cron.rb +5 -6
  107. data/lib/chef/provider/deploy.rb +43 -10
  108. data/lib/chef/provider/deploy/revision.rb +2 -3
  109. data/lib/chef/provider/erl_call.rb +72 -0
  110. data/lib/chef/provider/file.rb +8 -4
  111. data/lib/chef/provider/git.rb +10 -5
  112. data/lib/chef/provider/group/dscl.rb +128 -0
  113. data/lib/chef/provider/http_request.rb +6 -2
  114. data/lib/chef/provider/ifconfig.rb +1 -0
  115. data/lib/chef/provider/link.rb +1 -1
  116. data/lib/chef/provider/log.rb +53 -0
  117. data/lib/chef/provider/mdadm.rb +88 -0
  118. data/lib/chef/provider/mount/mount.rb +1 -1
  119. data/lib/chef/provider/package.rb +1 -1
  120. data/lib/chef/provider/package/easy_install.rb +106 -0
  121. data/lib/chef/provider/package/pacman.rb +101 -0
  122. data/lib/chef/provider/package/portage.rb +1 -1
  123. data/lib/chef/provider/package/rpm.rb +10 -8
  124. data/lib/chef/provider/package/yum-dump.py +22 -3
  125. data/lib/chef/provider/package/yum.rb +32 -8
  126. data/lib/chef/provider/package/zypper.rb +132 -0
  127. data/lib/chef/provider/remote_directory.rb +58 -49
  128. data/lib/chef/provider/remote_file.rb +1 -1
  129. data/lib/chef/provider/route.rb +136 -80
  130. data/lib/chef/provider/ruby_block.rb +18 -1
  131. data/lib/chef/provider/service/arch.rb +109 -0
  132. data/lib/chef/provider/service/freebsd.rb +0 -1
  133. data/lib/chef/provider/service/simple.rb +2 -3
  134. data/lib/chef/provider/service/upstart.rb +191 -0
  135. data/lib/chef/provider/subversion.rb +12 -4
  136. data/lib/chef/provider/template.rb +85 -53
  137. data/lib/chef/provider/user.rb +1 -1
  138. data/lib/chef/provider/user/dscl.rb +277 -0
  139. data/lib/chef/provider/user/useradd.rb +1 -0
  140. data/lib/chef/recipe.rb +2 -41
  141. data/lib/chef/resource.rb +9 -3
  142. data/lib/chef/resource/breakpoint.rb +35 -0
  143. data/lib/chef/resource/deploy.rb +16 -2
  144. data/lib/chef/resource/easy_install_package.rb +41 -0
  145. data/lib/chef/resource/erl_call.rb +83 -0
  146. data/lib/chef/resource/freebsd_package.rb +35 -0
  147. data/lib/chef/resource/log.rb +62 -0
  148. data/lib/chef/resource/mdadm.rb +82 -0
  149. data/lib/chef/resource/pacman_package.rb +33 -0
  150. data/lib/chef/resource/ruby_block.rb +21 -2
  151. data/lib/chef/resource/scm.rb +8 -0
  152. data/lib/chef/resource/subversion.rb +1 -0
  153. data/lib/chef/resource/user.rb +5 -2
  154. data/lib/chef/resource/yum_package.rb +36 -0
  155. data/lib/chef/resource_collection.rb +17 -9
  156. data/lib/chef/resource_collection/stepable_iterator.rb +124 -0
  157. data/lib/chef/rest.rb +166 -81
  158. data/lib/chef/role.rb +114 -38
  159. data/lib/chef/run_list.rb +15 -6
  160. data/lib/chef/runner.rb +13 -11
  161. data/lib/chef/search/query.rb +60 -0
  162. data/lib/chef/shef.rb +220 -0
  163. data/lib/chef/shef/ext.rb +297 -0
  164. data/lib/chef/shef/shef_session.rb +175 -0
  165. data/lib/chef/streaming_cookbook_uploader.rb +187 -0
  166. data/lib/chef/tasks/chef_repo.rake +53 -155
  167. data/lib/chef/util/file_edit.rb +94 -96
  168. data/lib/chef/webui_user.rb +233 -0
  169. metadata +219 -63
  170. data/distro/debian/etc/init.d/chef-indexer +0 -175
  171. data/distro/redhat/etc/chef/client.rb +0 -16
  172. data/distro/redhat/etc/chef/indexer.rb +0 -10
  173. data/distro/redhat/etc/chef/server.rb +0 -22
  174. data/distro/redhat/etc/init.d/chef-indexer +0 -76
  175. data/lib/chef/application/indexer.rb +0 -141
  176. data/lib/chef/queue.rb +0 -145
  177. data/lib/chef/search.rb +0 -88
  178. data/lib/chef/search/result.rb +0 -64
  179. data/lib/chef/search_index.rb +0 -77
  180. data/lib/chef/util/fileedit.rb +0 -121
@@ -0,0 +1,170 @@
1
+ #
2
+ # Author:: Adam Jacob (<adam@opscode.com>)
3
+ # Copyright:: Copyright (c) 2009 Opscode, Inc.
4
+ # License:: Apache License, Version 2.0
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ require 'chef/knife'
20
+ require 'chef/data_bag_item'
21
+
22
+ class Chef
23
+ class Knife
24
+ class Ssh < Knife
25
+
26
+ banner "Sub-Command: ssh QUERY COMMAND (options)"
27
+
28
+ option :concurrency,
29
+ :short => "-C NUM",
30
+ :long => "--concurrency NUM",
31
+ :description => "The number of concurrent connections",
32
+ :default => nil
33
+
34
+ option :attribute,
35
+ :short => "-a ATTR",
36
+ :long => "--attribute ATTR",
37
+ :description => "The attribute to use for opening the connection - default is fqdn",
38
+ :default => "fqdn"
39
+
40
+ def session
41
+ @session ||= Net::SSH::Multi.start(:concurrent_connections => config[:concurrency])
42
+ end
43
+
44
+
45
+ def h
46
+ @highline ||= HighLine.new
47
+ end
48
+
49
+ def configure_session
50
+ q = Chef::Search::Query.new
51
+ q.search(:node, @name_args[0]) do |item|
52
+ data = format_for_display(item)
53
+ Chef::Log.debug("Adding #{data[config[:attribute]]}")
54
+ session.use data[config[:attribute]]
55
+ @longest = data[config[:attribute]].length if data[config[:attribute]].length > @longest
56
+ end
57
+ end
58
+
59
+ def fixup_sudo(command)
60
+ command.sub(/^sudo/, 'sudo -p \'knife sudo password: \'')
61
+ end
62
+
63
+ def print_data(host, data)
64
+ if data =~ /\n/
65
+ data.split(/\n/).each { |d| print_data(host, d) }
66
+ else
67
+ padding = @longest - host.length
68
+ print h.color(host, :cyan)
69
+ padding.downto(0) { print " " }
70
+ puts data
71
+ end
72
+ end
73
+
74
+ def ssh_command(command, subsession=nil)
75
+ subsession ||= session
76
+ command = fixup_sudo(command)
77
+ subsession.open_channel do |ch|
78
+ ch.request_pty
79
+ ch.exec command do |ch, success|
80
+ raise ArgumentError, "Cannot execute #{command}" unless success
81
+ ch.on_data do |ichannel, data|
82
+ print_data(ichannel[:host], data)
83
+ if data =~ /^knife sudo password: /
84
+ ichannel.send_data("#{get_password}\n")
85
+ end
86
+ end
87
+ end
88
+ end
89
+ session.loop
90
+ end
91
+
92
+ def get_password
93
+ @password ||= h.ask("Enter your password: ") { |q| q.echo = false }
94
+ end
95
+
96
+ # Present the prompt and read a single line from the console. It also
97
+ # detects ^D and returns "exit" in that case. Adds the input to the
98
+ # history, unless the input is empty. Loops repeatedly until a non-empty
99
+ # line is input.
100
+ def read_line
101
+ loop do
102
+ command = reader.readline("#{h.color('knife-ssh>', :bold)} ", true)
103
+
104
+ if command.nil?
105
+ command = "exit"
106
+ puts(command)
107
+ else
108
+ command.strip!
109
+ end
110
+
111
+ unless command.empty?
112
+ return command
113
+ end
114
+ end
115
+ end
116
+
117
+ def reader
118
+ Readline
119
+ end
120
+
121
+ def interactive
122
+ puts "Connected to #{h.list(session.servers_for.collect { |s| h.color(s.host, :cyan) }, :inline, " and ")}"
123
+ puts
124
+ puts "To run a command on a list of servers, do:"
125
+ puts " on SERVER1 SERVER2 SERVER3; COMMAND"
126
+ puts " Example: on latte foamy; echo foobar"
127
+ puts
128
+ puts "To exit interactive mode, use 'quit!'"
129
+ puts
130
+ while 1
131
+ command = read_line
132
+ case command
133
+ when 'quit!'
134
+ puts 'Bye!'
135
+ break
136
+ when /^on (.+?); (.+)$/
137
+ raw_list = $1.split(" ")
138
+ server_list = Array.new
139
+ session.servers.each do |session_server|
140
+ server_list << session_server if raw_list.include?(session_server.host)
141
+ end
142
+ command = $2
143
+ ssh_command(command, session.on(*server_list))
144
+ else
145
+ ssh_command(command)
146
+ end
147
+ end
148
+ end
149
+
150
+ def run
151
+ @longest = 0
152
+
153
+ require 'net/ssh/multi'
154
+ require 'readline'
155
+ require 'highline'
156
+
157
+ configure_session
158
+
159
+ if @name_args[1] == "interactive"
160
+ interactive
161
+ else
162
+ ssh_command(@name_args[1..-1].join(" "))
163
+ end
164
+
165
+ session.close
166
+ end
167
+ end
168
+ end
169
+ end
170
+
@@ -1,6 +1,7 @@
1
1
  #
2
2
  # Author:: Adam Jacob (<adam@opscode.com>)
3
- # Author:: AJ Christensen (<@aj@opsocde.com>)
3
+ # Author:: AJ Christensen (<@aj@opscode.com>)
4
+ # Author:: Christopher Brown (<cb@opscode.com>)
4
5
  # Copyright:: Copyright (c) 2008 Opscode, Inc.
5
6
  # License:: Apache License, Version 2.0
6
7
  #
@@ -22,18 +23,39 @@ require 'mixlib/log'
22
23
  class Chef
23
24
  class Log
24
25
  extend Mixlib::Log
26
+
27
+ class << self
28
+ attr_accessor :verbose
29
+ attr_reader :verbose_logger
30
+ protected :verbose_logger
31
+
32
+ def verbose
33
+ !(@verbose_logger.nil?)
34
+ end
25
35
 
26
- # This is here for compatability, before we moved to
27
- # Mixlib::Log.
28
- class Formatter
29
- def self.show_time=(arg)
30
- Mixlib::Log::Formatter.show_time = arg
36
+ def verbose=(value)
37
+ if value
38
+ @verbose_logger ||= Logger.new(STDOUT)
39
+ @verbose_logger.level = self.logger.level
40
+ @verbose_logger.formatter = self.logger.formatter
41
+ else
42
+ @verbose_logger = nil
43
+ end
44
+ self.verbose
31
45
  end
46
+
47
+ def method_missing(method_symbol, *args)
48
+ self.verbose_logger.send(method_symbol, *args) if self.verbose
49
+ logger.send(method_symbol, *args)
50
+ end
51
+ end
32
52
 
33
- def self.show_time
34
- Mixlib::Log::Formatter.show_time
53
+ class Formatter
54
+ def self.show_time=(*args)
55
+ Mixlib::Log::Formatter.show_time = *args
35
56
  end
36
57
  end
58
+
37
59
  end
38
60
  end
39
61
 
@@ -17,19 +17,14 @@
17
17
  #
18
18
 
19
19
  require 'digest/sha2'
20
+ require 'chef/cache/checksum'
20
21
 
21
22
  class Chef
22
23
  module Mixin
23
24
  module Checksum
24
25
 
25
26
  def checksum(file)
26
- digest = Digest::SHA256.new
27
- fh = ::File.open(file)
28
- fh.each do |line|
29
- digest.update(line)
30
- end
31
- fh.close
32
- digest.hexdigest
27
+ Chef::Cache::Checksum.checksum_for_file(file)
33
28
  end
34
29
 
35
30
  end
@@ -39,14 +39,16 @@ class Chef
39
39
  # === Returns
40
40
  # true:: Returns true if the block is true, or if the command returns 0
41
41
  # false:: Returns false if the block is false, or if the command returns a non-zero exit code.
42
- def only_if(command)
42
+ def only_if(command, args = {})
43
43
  if command.kind_of?(Proc)
44
- res = command.call
45
- unless res
46
- return false
44
+ chdir_or_tmpdir(args[:cwd]) do
45
+ res = command.call
46
+ unless res
47
+ return false
48
+ end
47
49
  end
48
50
  else
49
- status = run_command(:command => command, :ignore_failure => true)
51
+ status = run_command({:command => command, :ignore_failure => true}.merge(args))
50
52
  if status.exitstatus != 0
51
53
  return false
52
54
  end
@@ -68,14 +70,16 @@ class Chef
68
70
  # === Returns
69
71
  # true:: Returns true if the block is false, or if the command returns a non-zero exit status.
70
72
  # false:: Returns false if the block is true, or if the command returns a 0 exit status.
71
- def not_if(command)
73
+ def not_if(command, args = {})
72
74
  if command.kind_of?(Proc)
73
- res = command.call
74
- if res
75
- return false
75
+ chdir_or_tmpdir(args[:cwd]) do
76
+ res = command.call
77
+ if res
78
+ return false
79
+ end
76
80
  end
77
81
  else
78
- status = run_command(:command => command, :ignore_failure => true)
82
+ status = run_command({:command => command, :ignore_failure => true}.merge(args))
79
83
  if status.exitstatus == 0
80
84
  return false
81
85
  end
@@ -130,7 +134,7 @@ class Chef
130
134
  stdout_string, stderr_string = stdout.string.chomp, stderr.string.chomp
131
135
  end
132
136
 
133
- args[:cwd] ||= Dir.tmpdir
137
+ args[:cwd] ||= Dir.tmpdir
134
138
  unless File.directory?(args[:cwd])
135
139
  raise Chef::Exceptions::Exec, "#{args[:cwd]} does not exist or is not a directory"
136
140
  end
@@ -306,6 +310,9 @@ class Chef
306
310
  begin
307
311
  if args[:waitlast]
308
312
  b[cid, *pi]
313
+ # send EOF so that if the child process is reading from STDIN
314
+ # it will actually finish up and exit
315
+ pi[0].close_write
309
316
  Process.waitpid2(cid).last
310
317
  else
311
318
  # This took some doing.
@@ -341,6 +348,7 @@ class Chef
341
348
  channels_to_watch << stderr if !stderr_finished
342
349
  ready = IO.select(channels_to_watch, nil, nil, 1.0)
343
350
  rescue Errno::EAGAIN
351
+ ensure
344
352
  results = Process.waitpid2(cid, Process::WNOHANG)
345
353
  if results
346
354
  stdout_finished = true
@@ -365,11 +373,11 @@ class Chef
365
373
  end
366
374
  end
367
375
  end
368
- results = Process.waitpid2(cid).last unless results
376
+ results = Process.waitpid2(cid) unless results
369
377
  o.rewind
370
378
  e.rewind
371
379
  b[cid, pi[0], o, e]
372
- results
380
+ results.last
373
381
  end
374
382
  ensure
375
383
  pi.each{|fd| fd.close unless fd.closed?}
@@ -381,6 +389,17 @@ class Chef
381
389
 
382
390
  module_function :popen4
383
391
 
392
+ def chdir_or_tmpdir(dir, &block)
393
+ dir ||= Dir.tmpdir
394
+ unless File.directory?(dir)
395
+ raise Chef::Exceptions::Exec, "#{dir} does not exist or is not a directory"
396
+ end
397
+ Dir.chdir(dir) do
398
+ block.call
399
+ end
400
+ end
401
+
402
+ module_function :chdir_or_tmpdir
384
403
  end
385
404
  end
386
405
  end
@@ -20,6 +20,7 @@
20
20
  class Chef
21
21
  module Mixin
22
22
  module ConvertToClassName
23
+ extend self
23
24
 
24
25
  def convert_to_class_name(str)
25
26
  rname = nil
@@ -38,6 +39,20 @@ class Chef
38
39
  rname
39
40
  end
40
41
 
42
+ def convert_to_snake_case(str, namespace=nil)
43
+ str = str.dup
44
+ str.sub!(/^#{namespace}(\:\:)?/, '') if namespace
45
+ str.gsub!(/[A-Z]/) {|s| "_" + s}
46
+ str.downcase!
47
+ str.sub!(/^\_/, "")
48
+ str
49
+ end
50
+
51
+ def snake_case_basename(str)
52
+ with_namespace = convert_to_snake_case(str)
53
+ with_namespace.split("::").last.sub(/^_/, '')
54
+ end
55
+
41
56
  def filename_to_qualified_string(base, filename)
42
57
  file_base = File.basename(filename, ".rb")
43
58
  base.to_s + (file_base == 'default' ? '' : "_#{file_base}")
@@ -1,6 +1,8 @@
1
1
  #
2
2
  # Author:: Adam Jacob (<adam@opscode.com>)
3
+ # Author:: Steve Midgley (http://www.misuse.org/science)
3
4
  # Copyright:: Copyright (c) 2009 Opscode, Inc.
5
+ # Copyright:: Copyright (c) 2008 Steve Midgley
4
6
  # License:: Apache License, Version 2.0
5
7
  #
6
8
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -15,22 +17,208 @@
15
17
  # See the License for the specific language governing permissions and
16
18
  # limitations under the License.
17
19
 
20
+ # Notice:
21
+ # This code is imported from deep_merge by Steve Midgley. deep_merge is
22
+ # available under the MIT license from
23
+ # http://trac.misuse.org/science/wiki/DeepMerge
24
+
18
25
  class Chef
19
26
  module Mixin
20
- class DeepMerge
27
+ module DeepMerge
21
28
  def self.merge(first, second)
22
- first = Mash.new(first).to_hash unless second.kind_of?(Mash)
23
- first = first.to_hash
24
- second = Mash.new(second).to_hash unless second.kind_of?(Mash)
25
- second = second.to_hash
26
- # Originally From: http://www.ruby-forum.com/topic/142809
27
- # Author: Stefan Rusterholz
28
- merger = proc do |key,v1,v2|
29
- v1.respond_to?(:keys) && v2.respond_to?(:keys) ? v1.merge(v2, &merger) : v2
30
- end
29
+ first = Mash.new(first) unless first.kind_of?(Mash)
30
+ second = Mash.new(second) unless second.kind_of?(Mash)
31
31
 
32
- Mash.new(first.merge(second, &merger))
32
+ DeepMerge.deep_merge!(second, first, {:knockout_prefix => "!merge:", :preserve_unmergeables => false})
33
+ end
34
+
35
+ class InvalidParameter < StandardError; end
36
+
37
+ DEFAULT_FIELD_KNOCKOUT_PREFIX = '--' unless defined?(DEFAULT_FIELD_KNOCKOUT_PREFIX)
38
+
39
+ # Deep Merge core documentation.
40
+ # deep_merge! method permits merging of arbitrary child elements. The two top level
41
+ # elements must be hashes. These hashes can contain unlimited (to stack limit) levels
42
+ # of child elements. These child elements to not have to be of the same types.
43
+ # Where child elements are of the same type, deep_merge will attempt to merge them together.
44
+ # Where child elements are not of the same type, deep_merge will skip or optionally overwrite
45
+ # the destination element with the contents of the source element at that level.
46
+ # So if you have two hashes like this:
47
+ # source = {:x => [1,2,3], :y => 2}
48
+ # dest = {:x => [4,5,'6'], :y => [7,8,9]}
49
+ # dest.deep_merge!(source)
50
+ # Results: {:x => [1,2,3,4,5,'6'], :y => 2}
51
+ # By default, "deep_merge!" will overwrite any unmergeables and merge everything else.
52
+ # To avoid this, use "deep_merge" (no bang/exclamation mark)
53
+ #
54
+ # Options:
55
+ # Options are specified in the last parameter passed, which should be in hash format:
56
+ # hash.deep_merge!({:x => [1,2]}, {:knockout_prefix => '--'})
57
+ # :preserve_unmergeables DEFAULT: false
58
+ # Set to true to skip any unmergeable elements from source
59
+ # :knockout_prefix DEFAULT: nil
60
+ # Set to string value to signify prefix which deletes elements from existing element
61
+ # :sort_merged_arrays DEFAULT: false
62
+ # Set to true to sort all arrays that are merged together
63
+ # :unpack_arrays DEFAULT: nil
64
+ # Set to string value to run "Array::join" then "String::split" against all arrays
65
+ # :merge_debug DEFAULT: false
66
+ # Set to true to get console output of merge process for debugging
67
+ #
68
+ # Selected Options Details:
69
+ # :knockout_prefix => The purpose of this is to provide a way to remove elements
70
+ # from existing Hash by specifying them in a special way in incoming hash
71
+ # source = {:x => ['--1', '2']}
72
+ # dest = {:x => ['1', '3']}
73
+ # dest.ko_deep_merge!(source)
74
+ # Results: {:x => ['2','3']}
75
+ # Additionally, if the knockout_prefix is passed alone as a string, it will cause
76
+ # the entire element to be removed:
77
+ # source = {:x => '--'}
78
+ # dest = {:x => [1,2,3]}
79
+ # dest.ko_deep_merge!(source)
80
+ # Results: {:x => ""}
81
+ # :unpack_arrays => The purpose of this is to permit compound elements to be passed
82
+ # in as strings and to be converted into discrete array elements
83
+ # irsource = {:x => ['1,2,3', '4']}
84
+ # dest = {:x => ['5','6','7,8']}
85
+ # dest.deep_merge!(source, {:unpack_arrays => ','})
86
+ # Results: {:x => ['1','2','3','4','5','6','7','8'}
87
+ # Why: If receiving data from an HTML form, this makes it easy for a checkbox
88
+ # to pass multiple values from within a single HTML element
89
+ #
90
+ # There are many tests for this library - and you can learn more about the features
91
+ # and usages of deep_merge! by just browsing the test examples
92
+ def self.deep_merge!(source, dest, options = {})
93
+ # turn on this line for stdout debugging text
94
+ merge_debug = options[:merge_debug] || false
95
+ overwrite_unmergeable = !options[:preserve_unmergeables]
96
+ knockout_prefix = options[:knockout_prefix] || nil
97
+ raise InvalidParameter, "knockout_prefix cannot be an empty string in deep_merge!" if knockout_prefix == ""
98
+ raise InvalidParameter, "overwrite_unmergeable must be true if knockout_prefix is specified in deep_merge!" if knockout_prefix && !overwrite_unmergeable
99
+ # if present: we will split and join arrays on this char before merging
100
+ array_split_char = options[:unpack_arrays] || false
101
+ # request that we sort together any arrays when they are merged
102
+ sort_merged_arrays = options[:sort_merged_arrays] || false
103
+ di = options[:debug_indent] || ''
104
+ # do nothing if source is nil
105
+ return dest if source.nil?
106
+ # if dest doesn't exist, then simply copy source to it
107
+ if dest.nil? && overwrite_unmergeable
108
+ dest = source; return dest
109
+ end
110
+
111
+ puts "#{di}Source class: #{source.class.inspect} :: Dest class: #{dest.class.inspect}" if merge_debug
112
+ if source.kind_of?(Hash)
113
+ puts "#{di}Hashes: #{source.inspect} :: #{dest.inspect}" if merge_debug
114
+ source.each do |src_key, src_value|
115
+ if dest.kind_of?(Hash)
116
+ puts "#{di} looping: #{src_key.inspect} => #{src_value.inspect} :: #{dest.inspect}" if merge_debug
117
+ if dest[src_key]
118
+ puts "#{di} ==>merging: #{src_key.inspect} => #{src_value.inspect} :: #{dest[src_key].inspect}" if merge_debug
119
+ dest[src_key] = deep_merge!(src_value, dest[src_key], options.merge(:debug_indent => di + ' '))
120
+ else # dest[src_key] doesn't exist so we want to create and overwrite it (but we do this via deep_merge!)
121
+ puts "#{di} ==>merging over: #{src_key.inspect} => #{src_value.inspect}" if merge_debug
122
+ # note: we rescue here b/c some classes respond to "dup" but don't implement it (Numeric, TrueClass, FalseClass, NilClass among maybe others)
123
+ begin
124
+ src_dup = src_value.dup # we dup src_value if possible because we're going to merge into it (since dest is empty)
125
+ rescue TypeError
126
+ src_dup = src_value
127
+ end
128
+ dest[src_key] = deep_merge!(src_value, src_dup, options.merge(:debug_indent => di + ' '))
129
+ end
130
+ else # dest isn't a hash, so we overwrite it completely (if permitted)
131
+ if overwrite_unmergeable
132
+ puts "#{di} overwriting dest: #{src_key.inspect} => #{src_value.inspect} -over-> #{dest.inspect}" if merge_debug
133
+ dest = overwrite_unmergeables(source, dest, options)
134
+ end
135
+ end
136
+ end
137
+ elsif source.kind_of?(Array)
138
+ puts "#{di}Arrays: #{source.inspect} :: #{dest.inspect}" if merge_debug
139
+ # if we are instructed, join/split any source arrays before processing
140
+ if array_split_char
141
+ puts "#{di} split/join on source: #{source.inspect}" if merge_debug
142
+ source = source.join(array_split_char).split(array_split_char)
143
+ if dest.kind_of?(Array)
144
+ dest = dest.join(array_split_char).split(array_split_char)
145
+ end
146
+ end
147
+ # if there's a naked knockout_prefix in source, that means we are to truncate dest
148
+ if source.index(knockout_prefix)
149
+ dest = clear_or_nil(dest); source.delete(knockout_prefix)
150
+ end
151
+ if dest.kind_of?(Array)
152
+ if knockout_prefix
153
+ print "#{di} knocking out: " if merge_debug
154
+ # remove knockout prefix items from both source and dest
155
+ source.delete_if do |ko_item|
156
+ retval = false
157
+ item = ko_item.respond_to?(:gsub) ? ko_item.gsub(%r{^#{knockout_prefix}}, "") : ko_item
158
+ if item != ko_item
159
+ print "#{ko_item} - " if merge_debug
160
+ dest.delete(item)
161
+ dest.delete(ko_item)
162
+ retval = true
163
+ end
164
+ retval
165
+ end
166
+ puts if merge_debug
167
+ end
168
+ puts "#{di} merging arrays: #{source.inspect} :: #{dest.inspect}" if merge_debug
169
+ dest = dest | source
170
+ dest.sort! if sort_merged_arrays
171
+ elsif overwrite_unmergeable
172
+ puts "#{di} overwriting dest: #{source.inspect} -over-> #{dest.inspect}" if merge_debug
173
+ dest = overwrite_unmergeables(source, dest, options)
174
+ end
175
+ else # src_hash is not an array or hash, so we'll have to overwrite dest
176
+ puts "#{di}Others: #{source.inspect} :: #{dest.inspect}" if merge_debug
177
+ dest = overwrite_unmergeables(source, dest, options)
178
+ end
179
+ puts "#{di}Returning #{dest.inspect}" if merge_debug
180
+ dest
181
+ end # deep_merge!
182
+
183
+ # allows deep_merge! to uniformly handle overwriting of unmergeable entities
184
+ def self.overwrite_unmergeables(source, dest, options)
185
+ merge_debug = options[:merge_debug] || false
186
+ overwrite_unmergeable = !options[:preserve_unmergeables]
187
+ knockout_prefix = options[:knockout_prefix] || false
188
+ di = options[:debug_indent] || ''
189
+ if knockout_prefix && overwrite_unmergeable
190
+ if source.kind_of?(String) # remove knockout string from source before overwriting dest
191
+ src_tmp = source.gsub(%r{^#{knockout_prefix}},"")
192
+ elsif source.kind_of?(Array) # remove all knockout elements before overwriting dest
193
+ src_tmp = source.delete_if {|ko_item| ko_item.kind_of?(String) && ko_item.match(%r{^#{knockout_prefix}}) }
194
+ else
195
+ src_tmp = source
196
+ end
197
+ if src_tmp == source # if we didn't find a knockout_prefix then we just overwrite dest
198
+ puts "#{di}#{src_tmp.inspect} -over-> #{dest.inspect}" if merge_debug
199
+ dest = src_tmp
200
+ else # if we do find a knockout_prefix, then we just delete dest
201
+ puts "#{di}\"\" -over-> #{dest.inspect}" if merge_debug
202
+ dest = ""
203
+ end
204
+ elsif overwrite_unmergeable
205
+ dest = source
206
+ end
207
+ dest
33
208
  end
209
+
210
+ def self.clear_or_nil(obj)
211
+ if obj.respond_to?(:clear)
212
+ obj.clear
213
+ else
214
+ obj = nil
215
+ end
216
+ obj
217
+ end
218
+
34
219
  end
220
+
35
221
  end
36
222
  end
223
+
224
+