chef 0.9.18 → 0.10.0.beta.0

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 (177) hide show
  1. data/README.rdoc +0 -3
  2. data/distro/arch/etc/rc.d/chef-server +0 -4
  3. data/distro/arch/etc/rc.d/chef-server-webui +0 -4
  4. data/distro/arch/etc/rc.d/chef-solr +0 -4
  5. data/distro/arch/etc/rc.d/chef-solr-indexer +0 -4
  6. data/lib/chef.rb +3 -3
  7. data/lib/chef/api_client.rb +1 -1
  8. data/lib/chef/application.rb +11 -1
  9. data/lib/chef/application/client.rb +18 -22
  10. data/lib/chef/application/knife.rb +28 -29
  11. data/lib/chef/application/solo.rb +14 -12
  12. data/lib/chef/client.rb +112 -54
  13. data/lib/chef/config.rb +4 -0
  14. data/lib/chef/cookbook/chefignore.rb +66 -0
  15. data/lib/chef/cookbook/cookbook_collection.rb +6 -5
  16. data/lib/chef/cookbook/cookbook_version_loader.rb +151 -0
  17. data/lib/chef/cookbook/file_system_file_vendor.rb +10 -8
  18. data/lib/chef/cookbook/metadata.rb +200 -108
  19. data/lib/chef/cookbook_loader.rb +39 -163
  20. data/lib/chef/cookbook_uploader.rb +100 -78
  21. data/lib/chef/cookbook_version.rb +92 -47
  22. data/lib/chef/cookbook_version_selector.rb +163 -0
  23. data/lib/chef/couchdb.rb +9 -1
  24. data/lib/chef/data_bag.rb +1 -1
  25. data/lib/chef/data_bag_item.rb +1 -1
  26. data/lib/chef/encrypted_data_bag_item.rb +126 -0
  27. data/lib/chef/environment.rb +386 -0
  28. data/lib/chef/exceptions.rb +82 -1
  29. data/lib/chef/index_queue/amqp_client.rb +15 -12
  30. data/lib/chef/index_queue/indexable.rb +38 -4
  31. data/lib/chef/json_compat.rb +3 -3
  32. data/lib/chef/knife.rb +97 -202
  33. data/lib/chef/knife/bootstrap.rb +27 -61
  34. data/lib/chef/knife/bootstrap/archlinux-gems.erb +4 -2
  35. data/lib/chef/knife/bootstrap/centos5-gems.erb +6 -15
  36. data/lib/chef/knife/bootstrap/fedora13-gems.erb +3 -4
  37. data/lib/chef/knife/bootstrap/ubuntu10.04-apt.erb +2 -2
  38. data/lib/chef/knife/bootstrap/ubuntu10.04-gems.erb +6 -5
  39. data/lib/chef/knife/client_bulk_delete.rb +6 -3
  40. data/lib/chef/knife/client_create.rb +13 -10
  41. data/lib/chef/knife/client_delete.rb +10 -7
  42. data/lib/chef/knife/client_edit.rb +9 -6
  43. data/lib/chef/knife/client_list.rb +8 -5
  44. data/lib/chef/knife/client_reregister.rb +9 -6
  45. data/lib/chef/knife/client_show.rb +9 -6
  46. data/lib/chef/knife/configure.rb +15 -19
  47. data/lib/chef/knife/configure_client.rb +4 -4
  48. data/lib/chef/knife/cookbook_bulk_delete.rb +11 -8
  49. data/lib/chef/knife/cookbook_create.rb +120 -55
  50. data/lib/chef/knife/cookbook_delete.rb +18 -12
  51. data/lib/chef/knife/cookbook_download.rb +10 -6
  52. data/lib/chef/knife/cookbook_list.rb +15 -6
  53. data/lib/chef/knife/cookbook_metadata.rb +41 -21
  54. data/lib/chef/knife/cookbook_metadata_from_file.rb +4 -0
  55. data/lib/chef/knife/cookbook_show.rb +16 -5
  56. data/lib/chef/knife/cookbook_site_download.rb +2 -2
  57. data/lib/chef/knife/cookbook_site_share.rb +18 -13
  58. data/lib/chef/knife/cookbook_site_unshare.rb +7 -4
  59. data/lib/chef/knife/cookbook_site_vendor.rb +21 -18
  60. data/lib/chef/knife/cookbook_test.rb +14 -14
  61. data/lib/chef/knife/cookbook_upload.rb +91 -40
  62. data/lib/chef/knife/data_bag_create.rb +41 -6
  63. data/lib/chef/knife/data_bag_delete.rb +5 -3
  64. data/lib/chef/knife/data_bag_edit.rb +55 -11
  65. data/lib/chef/knife/data_bag_from_file.rb +47 -7
  66. data/lib/chef/knife/data_bag_list.rb +4 -1
  67. data/lib/chef/knife/data_bag_show.rb +44 -4
  68. data/lib/chef/knife/environment_create.rb +53 -0
  69. data/lib/chef/knife/environment_delete.rb +45 -0
  70. data/lib/chef/knife/environment_edit.rb +45 -0
  71. data/lib/chef/knife/environment_from_file.rb +39 -0
  72. data/lib/chef/knife/environment_list.rb +42 -0
  73. data/lib/chef/knife/environment_show.rb +46 -0
  74. data/lib/chef/knife/exec.rb +1 -1
  75. data/lib/chef/knife/index_rebuild.rb +8 -9
  76. data/lib/chef/knife/node_bulk_delete.rb +9 -6
  77. data/lib/chef/knife/node_create.rb +9 -6
  78. data/lib/chef/knife/node_delete.rb +10 -7
  79. data/lib/chef/knife/node_edit.rb +129 -10
  80. data/lib/chef/knife/node_from_file.rb +10 -7
  81. data/lib/chef/knife/node_list.rb +11 -6
  82. data/lib/chef/knife/node_run_list_add.rb +10 -7
  83. data/lib/chef/knife/node_run_list_remove.rb +9 -6
  84. data/lib/chef/knife/node_show.rb +15 -7
  85. data/lib/chef/knife/recipe_list.rb +4 -3
  86. data/lib/chef/knife/role_bulk_delete.rb +9 -6
  87. data/lib/chef/knife/role_create.rb +9 -6
  88. data/lib/chef/knife/role_delete.rb +10 -7
  89. data/lib/chef/knife/role_edit.rb +11 -8
  90. data/lib/chef/knife/role_from_file.rb +10 -7
  91. data/lib/chef/knife/role_list.rb +8 -5
  92. data/lib/chef/knife/role_show.rb +11 -8
  93. data/lib/chef/knife/search.rb +33 -10
  94. data/lib/chef/knife/ssh.rb +33 -61
  95. data/lib/chef/knife/status.rb +7 -4
  96. data/lib/chef/knife/subcommand_loader.rb +101 -0
  97. data/lib/chef/knife/tag_create.rb +31 -0
  98. data/lib/chef/knife/tag_delete.rb +31 -0
  99. data/lib/chef/knife/tag_list.rb +29 -0
  100. data/lib/chef/knife/ui.rb +229 -0
  101. data/lib/chef/knife/windows_bootstrap.rb +8 -5
  102. data/lib/chef/log.rb +5 -59
  103. data/lib/chef/mash.rb +211 -0
  104. data/lib/chef/mixins.rb +1 -2
  105. data/lib/chef/nil_argument.rb +3 -0
  106. data/lib/chef/node.rb +96 -34
  107. data/lib/chef/platform.rb +27 -0
  108. data/lib/chef/provider/cookbook_file.rb +21 -20
  109. data/lib/chef/provider/deploy/revision.rb +3 -0
  110. data/lib/chef/provider/file.rb +20 -11
  111. data/lib/chef/provider/git.rb +26 -26
  112. data/lib/chef/provider/group/aix.rb +70 -0
  113. data/lib/chef/provider/group/groupadd.rb +7 -4
  114. data/lib/chef/provider/group/usermod.rb +1 -1
  115. data/lib/chef/provider/package.rb +28 -28
  116. data/lib/chef/provider/package/dpkg.rb +1 -1
  117. data/lib/chef/provider/package/portage.rb +50 -39
  118. data/lib/chef/provider/package/rubygems.rb +1 -1
  119. data/lib/chef/provider/package/zypper.rb +3 -20
  120. data/lib/chef/provider/remote_directory.rb +0 -2
  121. data/lib/chef/provider/remote_file.rb +2 -3
  122. data/lib/chef/provider/service/arch.rb +28 -35
  123. data/lib/chef/provider/service/simple.rb +1 -1
  124. data/lib/chef/provider/subversion.rb +22 -22
  125. data/lib/chef/providers.rb +1 -0
  126. data/lib/chef/recipe.rb +10 -12
  127. data/lib/chef/resource.rb +49 -42
  128. data/lib/chef/resource/gem_package.rb +7 -3
  129. data/lib/chef/resource/git.rb +5 -5
  130. data/lib/chef/resource/package.rb +7 -7
  131. data/lib/chef/resource/scm.rb +2 -1
  132. data/lib/chef/resource/solaris_package.rb +0 -1
  133. data/lib/chef/resource/yum_package.rb +0 -1
  134. data/lib/chef/rest.rb +7 -16
  135. data/lib/chef/rest/rest_request.rb +0 -16
  136. data/lib/chef/role.rb +67 -13
  137. data/lib/chef/run_context.rb +37 -21
  138. data/lib/chef/run_list.rb +30 -15
  139. data/lib/chef/run_list/run_list_expansion.rb +41 -20
  140. data/lib/chef/run_list/run_list_item.rb +20 -6
  141. data/lib/chef/run_list/versioned_recipe_list.rb +68 -0
  142. data/lib/chef/runner.rb +7 -15
  143. data/lib/chef/search/query.rb +12 -7
  144. data/lib/chef/shef.rb +6 -7
  145. data/lib/chef/shef/shef_session.rb +40 -35
  146. data/lib/chef/shell_out.rb +22 -201
  147. data/lib/chef/shell_out/unix.rb +224 -0
  148. data/lib/chef/shell_out/windows.rb +95 -0
  149. data/lib/chef/solr_query.rb +187 -0
  150. data/lib/chef/solr_query/lucene.treetop +145 -0
  151. data/lib/chef/solr_query/lucene_nodes.rb +285 -0
  152. data/lib/chef/solr_query/query_transform.rb +65 -0
  153. data/lib/chef/solr_query/solr_http_request.rb +118 -0
  154. data/lib/chef/version.rb +4 -2
  155. data/lib/chef/version_class.rb +70 -0
  156. data/lib/chef/version_constraint.rb +116 -0
  157. metadata +68 -37
  158. data/lib/chef/cookbook/metadata/version.rb +0 -87
  159. data/lib/chef/knife/bluebox_images_list.rb +0 -54
  160. data/lib/chef/knife/bluebox_server_create.rb +0 -157
  161. data/lib/chef/knife/bluebox_server_delete.rb +0 -63
  162. data/lib/chef/knife/bluebox_server_list.rb +0 -59
  163. data/lib/chef/knife/ec2_instance_data.rb +0 -46
  164. data/lib/chef/knife/ec2_server_create.rb +0 -218
  165. data/lib/chef/knife/ec2_server_delete.rb +0 -87
  166. data/lib/chef/knife/ec2_server_list.rb +0 -89
  167. data/lib/chef/knife/rackspace_server_create.rb +0 -184
  168. data/lib/chef/knife/rackspace_server_delete.rb +0 -57
  169. data/lib/chef/knife/rackspace_server_list.rb +0 -59
  170. data/lib/chef/knife/slicehost_images_list.rb +0 -53
  171. data/lib/chef/knife/slicehost_server_create.rb +0 -103
  172. data/lib/chef/knife/slicehost_server_delete.rb +0 -61
  173. data/lib/chef/knife/slicehost_server_list.rb +0 -64
  174. data/lib/chef/knife/terremark_server_create.rb +0 -152
  175. data/lib/chef/knife/terremark_server_delete.rb +0 -87
  176. data/lib/chef/knife/terremark_server_list.rb +0 -77
  177. data/lib/chef/mixin/find_preferred_file.rb +0 -92
@@ -1,6 +1,7 @@
1
1
  #
2
2
  # Author:: Adam Jacob (<adam@opscode.com>)
3
- # Copyright:: Copyright (c) 2008 Opscode, Inc.
3
+ # Author:: Seth Falcon (<seth@opscode.com>)
4
+ # Copyright:: Copyright 2008-2010 Opscode, Inc.
4
5
  # License:: Apache License, Version 2.0
5
6
  #
6
7
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -49,6 +50,9 @@ class Chef
49
50
  class RedirectLimitExceeded < RuntimeError; end
50
51
  class AmbiguousRunlistSpecification < ArgumentError; end
51
52
  class CookbookNotFound < RuntimeError; end
53
+ # Cookbook loader used to raise an argument error when cookbook not found.
54
+ # for back compat, need to raise an error that inherits from ArgumentError
55
+ class CookbookNotFoundInRepo < ArgumentError; end
52
56
  class AttributeNotFound < RuntimeError; end
53
57
  class InvalidCommandOption < RuntimeError; end
54
58
  class CommandTimeout < RuntimeError; end
@@ -63,10 +67,87 @@ class Chef
63
67
  class InvalidResourceSpecification < ArgumentError; end
64
68
  class SolrConnectionError < RuntimeError; end
65
69
  class IllegalChecksumRevert < RuntimeError; end
70
+ class CookbookVersionNameMismatch < ArgumentError; end
66
71
  class MissingParentDirectory < RuntimeError; end
67
72
  class UnresolvableGitReference < RuntimeError; end
68
73
  class InvalidEnvironmentRunListSpecification < ArgumentError; end
69
74
  class InvalidDataBagItemID < ArgumentError; end
70
75
  class InvalidDataBagName < ArgumentError; end
76
+ class EnclosingDirectoryDoesNotExist < ArgumentError; end
77
+
78
+ class ObsoleteDependencySyntax < ArgumentError; end
79
+
80
+ # A different version of a cookbook was added to a
81
+ # VersionedRecipeList than the one already there.
82
+ class CookbookVersionConflict < ArgumentError ; end
83
+
84
+ # does not follow X.Y.Z format. ArgumentError?
85
+ class InvalidCookbookVersion < ArgumentError; end
86
+
87
+ # version constraint should be a string or array, or it doesn't
88
+ # match OP VERSION. ArgumentError?
89
+ class InvalidVersionConstraint < ArgumentError; end
90
+
91
+ class CookbookVersionSelection
92
+
93
+ # Compound exception: In run_list expansion and resolution,
94
+ # run_list items referred to cookbooks that don't exist and/or
95
+ # have no versions available.
96
+ class InvalidRunListItems < StandardError
97
+ attr_reader :non_existent_cookbooks
98
+ attr_reader :cookbooks_with_no_matching_versions
99
+
100
+ def initialize(message, non_existent_cookbooks, cookbooks_with_no_matching_versions)
101
+ super(message)
102
+
103
+ @non_existent_cookbooks = non_existent_cookbooks
104
+ @cookbooks_with_no_matching_versions = cookbooks_with_no_matching_versions
105
+ end
106
+
107
+ def to_json(*a)
108
+ result = {
109
+ "message" => message,
110
+ "non_existent_cookbooks" => non_existent_cookbooks,
111
+ "cookbooks_with_no_versions" => cookbooks_with_no_matching_versions
112
+ }
113
+ result.to_json(*a)
114
+ end
115
+ end
116
+
117
+ # In run_list expansion and resolution, a constraint was
118
+ # unsatisfiable.
119
+ #
120
+ # This exception may not be the complete error report. If you
121
+ # resolve the misconfiguration represented by this exception and
122
+ # re-solve, you may get another exception
123
+ class UnsatisfiableRunListItem < StandardError
124
+ attr_reader :run_list_item
125
+ attr_reader :non_existent_cookbooks, :most_constrained_cookbooks
126
+
127
+ # most_constrained_cookbooks: if I were to remove constraints
128
+ # regarding these cookbooks, I would get a solution or move on
129
+ # to the next error (deeper in the graph). An item in this list
130
+ # may be unsatisfiable, but when resolved may also reveal
131
+ # further unsatisfiable constraints; this condition would not be
132
+ # reported.
133
+ def initialize(message, run_list_item, non_existent_cookbooks, most_constrained_cookbooks)
134
+ super(message)
135
+
136
+ @run_list_item = run_list_item
137
+ @non_existent_cookbooks = non_existent_cookbooks
138
+ @most_constrained_cookbooks = most_constrained_cookbooks
139
+ end
140
+
141
+ def to_json(*a)
142
+ result = {
143
+ "message" => message,
144
+ "unsatisfiable_run_list_item" => run_list_item,
145
+ "non_existent_cookbooks" => non_existent_cookbooks,
146
+ "most_constrained_cookbooks" => most_constrained_cookbooks
147
+ }
148
+ result.to_json(*a)
149
+ end
150
+ end
151
+ end
71
152
  end
72
153
  end
@@ -19,6 +19,8 @@
19
19
  class Chef
20
20
  module IndexQueue
21
21
  class AmqpClient
22
+ VNODES = 1024
23
+
22
24
  include Singleton
23
25
 
24
26
  def initialize
@@ -29,11 +31,9 @@ class Chef
29
31
  @amqp_client && amqp_client.connected? && amqp_client.stop
30
32
  @amqp_client = nil
31
33
  @exchange = nil
32
- @queue = nil
33
34
  end
34
35
 
35
36
  def stop
36
- @queue && @queue.subscription && @queue.unsubscribe
37
37
  @amqp_client && @amqp_client.stop
38
38
  end
39
39
 
@@ -59,24 +59,17 @@ class Chef
59
59
  @exchange ||= amqp_client.exchange("chef-indexer", :durable => true, :type => :fanout)
60
60
  end
61
61
 
62
- def queue
63
- unless @queue
64
- @queue = amqp_client.queue("chef-index-consumer-" + consumer_id, :durable => durable_queue?)
65
- @queue.bind(exchange)
66
- end
67
- @queue
68
- end
69
-
70
62
  def disconnected!
71
63
  Chef::Log.error("Disconnected from the AMQP Broker (RabbitMQ)")
72
64
  @amqp_client = nil
73
65
  reset!
74
66
  end
75
67
 
76
- def send_action(action, data)
68
+ def queue_for_object(obj_id)
77
69
  retries = 0
70
+ vnode_tag = obj_id_to_int(obj_id) % VNODES
78
71
  begin
79
- exchange.publish(Chef::JSONCompat.to_json({"action" => action.to_s, "payload" => data}))
72
+ yield amqp_client.queue("vnode-#{vnode_tag}", :passive => false, :durable => true, :exclusive => false, :auto_delete => false)
80
73
  rescue Bunny::ServerDownError, Bunny::ConnectionError, Errno::ECONNRESET
81
74
  disconnected!
82
75
  if (retries += 1) < 2
@@ -90,6 +83,15 @@ class Chef
90
83
  end
91
84
 
92
85
  private
86
+
87
+ # Sometimes object ids are "proper" UUIDs, like "64bc00eb-120b-b6a2-ec0e-34fc90d151be"
88
+ # and sometimes they omit the dashes, like "64bc00eb120bb6a2ec0e34fc90d151be"
89
+ # UUIDTools uses different methods to parse the different styles.
90
+ def obj_id_to_int(obj_id)
91
+ UUIDTools::UUID.parse(obj_id).to_i
92
+ rescue ArgumentError
93
+ UUIDTools::UUID.parse_hexdigest(obj_id).to_i
94
+ end
93
95
 
94
96
  def durable_queue?
95
97
  !!Chef::Config[:amqp_consumer_id]
@@ -111,3 +113,4 @@ class Chef
111
113
  end
112
114
  end
113
115
  end
116
+
@@ -1,6 +1,8 @@
1
1
  #
2
2
  # Author:: Daniel DeLeo (<dan@kallistec.com>)
3
+ # Author:: Seth Falcon (<seth@opscode.com>)
3
4
  # Copyright:: Copyright (c) 2009 Daniel DeLeo
5
+ # Copyright:: Copyright (c) 2010 Opscode, Inc.
4
6
  # License:: Apache License, Version 2.0
5
7
  #
6
8
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -15,6 +17,7 @@
15
17
  # See the License for the specific language governing permissions and
16
18
  # limitations under the License.
17
19
  #
20
+ require 'chef/json_compat'
18
21
 
19
22
  class Chef
20
23
  module IndexQueue
@@ -56,20 +59,51 @@ class Chef
56
59
  with_metadata["id"] ||= self.index_id
57
60
  with_metadata["database"] ||= Chef::Config[:couchdb_database]
58
61
  with_metadata["item"] ||= self.to_hash
62
+ with_metadata["enqueued_at"] ||= Time.now.utc.to_i
59
63
 
60
64
  raise ArgumentError, "Type, Id, or Database missing in index operation: #{with_metadata.inspect}" if (with_metadata["id"].nil? or with_metadata["type"].nil?)
61
65
  with_metadata
62
66
  end
63
-
67
+
64
68
  def add_to_index(metadata={})
65
- Chef::Log.debug("pushing item to index queue for addition: #{self.with_indexer_metadata(metadata)}")
66
- AmqpClient.instance.send_action(:add, self.with_indexer_metadata(metadata))
69
+ Chef::Log.debug("pushing item to index queue for addition: #{self.with_indexer_metadata(metadata)}")
70
+ object_with_metadata = with_indexer_metadata(metadata)
71
+ obj_id = object_with_metadata["id"]
72
+ obj = {:action => :add, :payload => self.with_indexer_metadata(metadata)}
73
+
74
+ publish_object(obj_id, obj)
67
75
  end
68
76
 
69
77
  def delete_from_index(metadata={})
70
78
  Chef::Log.debug("pushing item to index queue for deletion: #{self.with_indexer_metadata(metadata)}")
71
- AmqpClient.instance.send_action(:delete, self.with_indexer_metadata(metadata))
79
+ object_with_metadata = with_indexer_metadata(metadata)
80
+ obj_id = object_with_metadata["id"]
81
+ obj = {:action => :delete, :payload => self.with_indexer_metadata(metadata)}
82
+
83
+ publish_object(obj_id, obj)
84
+ end
85
+
86
+ private
87
+
88
+ # Uses the publisher to update the object's queue. If
89
+ # Chef::Config[:persistent_queue] is true, the update is wrapped
90
+ # in a transaction.
91
+ def publish_object(object_id, object)
92
+ publisher = AmqpClient.instance
93
+ begin
94
+ publisher.amqp_client.tx_select if Chef::Config[:persistent_queue]
95
+ publisher.queue_for_object(object_id) do |queue|
96
+ queue.publish(Chef::JSONCompat.to_json(object), :persistent => Chef::Config[:persistent_queue])
97
+ end
98
+ publisher.amqp_client.tx_commit if Chef::Config[:persistent_queue]
99
+ rescue
100
+ publisher.amqp_client.tx_rollback if Chef::Config[:persistent_queue]
101
+ raise
102
+ end
103
+
104
+ true
72
105
  end
106
+
73
107
  end
74
108
  end
75
109
  end
@@ -24,8 +24,9 @@ class Chef
24
24
  JSON_MAX_NESTING = 1000
25
25
 
26
26
  class <<self
27
- # See PL-538. Increase the max nesting for JSON, which defaults to 19,
28
- # and isn't enough for some deep node (for example) structures.
27
+ # See CHEF-1292/PL-538. Increase the max nesting for JSON, which defaults
28
+ # to 19, and isn't enough for some (for example, a Node within a Node)
29
+ # structures.
29
30
  def opts_add_max_nesting(opts)
30
31
  if opts.nil? || !opts.has_key?(:max_nesting)
31
32
  opts = opts.nil? ? Hash.new : opts.clone
@@ -40,7 +41,6 @@ class Chef
40
41
  end
41
42
 
42
43
  def to_json(obj, opts = nil)
43
- #::JSON.generate(obj, opts_add_max_nesting(opts))
44
44
  obj.to_json(opts_add_max_nesting(opts))
45
45
  end
46
46
 
@@ -7,9 +7,9 @@
7
7
  # Licensed under the Apache License, Version 2.0 (the "License");
8
8
  # you may not use this file except in compliance with the License.
9
9
  # You may obtain a copy of the License at
10
- #
10
+ #
11
11
  # http://www.apache.org/licenses/LICENSE-2.0
12
- #
12
+ #
13
13
  # Unless required by applicable law or agreed to in writing, software
14
14
  # distributed under the License is distributed on an "AS IS" BASIS,
15
15
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -17,28 +17,52 @@
17
17
  # limitations under the License.
18
18
  #
19
19
 
20
+ require 'forwardable'
20
21
  require 'chef/version'
21
22
  require 'mixlib/cli'
22
23
  require 'chef/mixin/convert_to_class_name'
23
- require 'chef/rest'
24
+ require 'chef/knife/subcommand_loader'
25
+ require 'chef/knife/ui'
26
+
24
27
  require 'pp'
25
28
 
26
29
  class Chef
27
30
  class Knife
28
-
29
- Chef::REST::RESTRequest.user_agent = "Chef Knife#{Chef::REST::RESTRequest::UA_COMMON}"
30
-
31
31
  include Mixlib::CLI
32
32
  extend Chef::Mixin::ConvertToClassName
33
-
34
- # The "require paths" of the core knife subcommands bundled with chef
35
- DEFAULT_SUBCOMMAND_FILES = Dir[File.expand_path(File.join(File.dirname(__FILE__), 'knife', '*.rb'))]
36
- DEFAULT_SUBCOMMAND_FILES.map! { |knife_file| knife_file[/#{CHEF_ROOT}#{Regexp.escape(File::SEPARATOR)}(.*)\.rb/,1] }
33
+ extend Forwardable
34
+
35
+ # Backwards Compat:
36
+ # Ideally, we should not vomit all of these methods into this base class;
37
+ # instead, they should be accessed by hitting the ui object directly.
38
+ def_delegator :@ui, :stdout
39
+ def_delegator :@ui, :stderr
40
+ def_delegator :@ui, :stdin
41
+ def_delegator :@ui, :msg
42
+ def_delegator :@ui, :ask_question
43
+ def_delegator :@ui, :pretty_print
44
+ def_delegator :@ui, :output
45
+ def_delegator :@ui, :format_list_for_display
46
+ def_delegator :@ui, :format_for_display
47
+ def_delegator :@ui, :format_cookbook_list_for_display
48
+ def_delegator :@ui, :edit_data
49
+ def_delegator :@ui, :edit_object
50
+ def_delegator :@ui, :confirm
37
51
 
38
52
  attr_accessor :name_args
53
+ attr_reader :ui
54
+
55
+ def self.ui
56
+ @ui ||= Chef::Knife::UI.new(STDOUT, STDERR, STDIN, {})
57
+ end
39
58
 
40
59
  def self.msg(msg="")
41
- puts msg
60
+ ui.msg(msg)
61
+ end
62
+
63
+ def self.reset_subcommands!
64
+ @@subcommands = {}
65
+ @subcommands_by_category = nil
42
66
  end
43
67
 
44
68
  def self.inherited(subclass)
@@ -73,6 +97,14 @@ class Chef
73
97
  name.nil? || name.empty?
74
98
  end
75
99
 
100
+ def self.subcommand_loader
101
+ @subcommand_loader ||= Knife::SubcommandLoader.new(chef_config_dir)
102
+ end
103
+
104
+ def self.load_commands
105
+ subcommand_loader.load_commands
106
+ end
107
+
76
108
  def self.subcommands
77
109
  @@subcommands ||= {}
78
110
  end
@@ -87,17 +119,10 @@ class Chef
87
119
  @subcommands_by_category
88
120
  end
89
121
 
90
- # Load all the sub-commands
91
- def self.load_commands
92
- DEFAULT_SUBCOMMAND_FILES.each { |subcommand| require subcommand }
93
- subcommands
94
- end
95
-
96
122
  # Print the list of subcommands knife knows about. If +preferred_category+
97
123
  # is given, only subcommands in that category are shown
98
124
  def self.list_commands(preferred_category=nil)
99
125
  load_commands
100
-
101
126
  category_desc = preferred_category ? preferred_category + " " : ''
102
127
  msg "Available #{category_desc}subcommands: (for details, knife SUB-COMMAND --help)\n\n"
103
128
 
@@ -125,7 +150,8 @@ class Chef
125
150
  def self.run(args, options={})
126
151
  load_commands
127
152
  subcommand_class = subcommand_class_from(args)
128
- subcommand_class.options.merge!(options)
153
+ subcommand_class.options = options.merge!(subcommand_class.options)
154
+ subcommand_class.load_deps
129
155
  instance = subcommand_class.new(args)
130
156
  instance.configure_chef
131
157
  instance.run
@@ -155,16 +181,12 @@ class Chef
155
181
  subcommand_class || subcommand_not_found!(args)
156
182
  end
157
183
 
158
- protected
184
+ def self.deps(&block)
185
+ @dependency_loader = block
186
+ end
159
187
 
160
- def load_late_dependency(dep, gem_name = nil)
161
- begin
162
- require dep
163
- rescue LoadError
164
- gem_name ||= dep.gsub('/', '-')
165
- Chef::Log.fatal "#{gem_name} is not installed. run \"gem install #{gem_name}\" to install it."
166
- exit 1
167
- end
188
+ def self.load_deps
189
+ @dependency_loader && @dependency_loader.call
168
190
  end
169
191
 
170
192
  private
@@ -174,9 +196,9 @@ class Chef
174
196
  # user could not be resolved to a subcommand.
175
197
  def self.subcommand_not_found!(args)
176
198
  unless want_help?(args)
177
- Chef::Log.fatal("Cannot find sub command for: '#{args.join(' ')}'")
199
+ ui.fatal("Cannot find sub command for: '#{args.join(' ')}'")
178
200
  end
179
- Chef::Knife.list_commands(guess_category(args))
201
+ list_commands(guess_category(args))
180
202
  exit 10
181
203
  end
182
204
 
@@ -187,12 +209,32 @@ class Chef
187
209
  (args.any? { |arg| arg =~ /^(:?(:?\-\-)?help|\-h)$/})
188
210
  end
189
211
 
212
+ @@chef_config_dir = nil
213
+
214
+ # search upward from current_dir until .chef directory is found
215
+ def self.chef_config_dir
216
+ if @@chef_config_dir.nil? # share this with subclasses
217
+ @@chef_config_dir = false
218
+ full_path = Dir.pwd.split(File::SEPARATOR)
219
+ (full_path.length - 1).downto(0) do |i|
220
+ canidate_directory = File.join(full_path[0..i] + [".chef" ])
221
+ if File.exist?(canidate_directory) && File.directory?(canidate_directory)
222
+ @@chef_config_dir = canidate_directory
223
+ break
224
+ end
225
+ end
226
+ end
227
+ @@chef_config_dir
228
+ end
229
+
230
+
190
231
  public
191
232
 
192
233
  # Create a new instance of the current class configured for the given
193
234
  # arguments and options
194
235
  def initialize(argv=[])
195
236
  super() # having to call super in initialize is the most annoying anti-pattern :(
237
+ @ui = Chef::Knife::UI.new(STDOUT, STDERR, STDIN, config)
196
238
 
197
239
  command_name_words = self.class.snake_case_name.split('_')
198
240
 
@@ -219,37 +261,11 @@ class Chef
219
261
  exit(1)
220
262
  end
221
263
 
222
- def ask_question(question, opts={})
223
- question = question + "[#{opts[:default]}] " if opts[:default]
224
-
225
- if opts[:default] and config[:defaults]
226
-
227
- opts[:default]
228
-
229
- else
230
-
231
- stdout.print question
232
- a = stdin.readline.strip
233
-
234
- if opts[:default]
235
- a.empty? ? opts[:default] : a
236
- else
237
- a
238
- end
239
-
240
- end
241
-
242
- end
243
-
244
264
  def configure_chef
245
265
  unless config[:config_file]
246
- full_path = Dir.pwd.split(File::SEPARATOR)
247
- (full_path.length - 1).downto(0) do |i|
248
- config_file_to_check = File.join([ full_path[0..i], ".chef", "knife.rb" ].flatten)
249
- if File.exists?(config_file_to_check)
250
- config[:config_file] = config_file_to_check
251
- break
252
- end
266
+ if self.class.chef_config_dir
267
+ candidate_config = File.expand_path('knife.rb',self.class.chef_config_dir)
268
+ config[:config_file] = candidate_config if File.exist?(candidate_config)
253
269
  end
254
270
  # If we haven't set a config yet and $HOME is set, and the home
255
271
  # knife.rb exists, use it:
@@ -266,13 +282,22 @@ class Chef
266
282
  self.msg("No knife configuration file found")
267
283
  end
268
284
 
269
- Chef::Config[:log_level] = config[:log_level] if config[:log_level]
270
- Chef::Config[:log_location] = config[:log_location] if config[:log_location]
271
- Chef::Config[:node_name] = config[:node_name] if config[:node_name]
272
- Chef::Config[:client_key] = config[:client_key] if config[:client_key]
273
- Chef::Config[:chef_server_url] = config[:chef_server_url] if config[:chef_server_url]
285
+ Chef::Config[:log_level] = config[:log_level] if config[:log_level]
286
+ Chef::Config[:log_location] = config[:log_location] if config[:log_location]
287
+ Chef::Config[:node_name] = config[:node_name] if config[:node_name]
288
+ Chef::Config[:client_key] = config[:client_key] if config[:client_key]
289
+ Chef::Config[:chef_server_url] = config[:chef_server_url] if config[:chef_server_url]
290
+ Chef::Config[:environment] = config[:environment] if config[:environment]
291
+
292
+ # Expand a relative path from the config directory. Config from command
293
+ # line should already be expanded, and absolute paths will be unchanged.
294
+ if Chef::Config[:client_key] && config[:config_file]
295
+ Chef::Config[:client_key] = File.expand_path(Chef::Config[:client_key], File.dirname(config[:config_file]))
296
+ end
297
+
298
+ Mixlib::Log::Formatter.show_time = false
274
299
  Chef::Log.init(Chef::Config[:log_location])
275
- Chef::Log.level(Chef::Config[:log_level])
300
+ Chef::Log.level(Chef::Config[:log_level] || :error)
276
301
 
277
302
  Chef::Log.debug("Using configuration from #{config[:config_file]}")
278
303
 
@@ -281,99 +306,6 @@ class Chef
281
306
  end
282
307
  end
283
308
 
284
- def pretty_print(data)
285
- puts data
286
- end
287
-
288
- def output(data)
289
- case config[:format]
290
- when "json", nil
291
- stdout.puts Chef::JSONCompat.to_json_pretty(data)
292
- when "yaml"
293
- require 'yaml'
294
- stdout.puts YAML::dump(data)
295
- when "text"
296
- # If you were looking for some attribute and there is only one match
297
- # just dump the attribute value
298
- if data.length == 1 and config[:attribute]
299
- stdout.puts data.values[0]
300
- else
301
- PP.pp(data, stdout)
302
- end
303
- else
304
- raise ArgumentError, "Unknown output format #{config[:format]}"
305
- end
306
- end
307
-
308
- def format_list_for_display(list)
309
- config[:with_uri] ? list : list.keys.sort { |a,b| a <=> b }
310
- end
311
-
312
- def format_for_display(item)
313
- data = item.kind_of?(Chef::DataBagItem) ? item.raw_data : item
314
-
315
- if config[:attribute]
316
- config[:attribute].split(".").each do |attr|
317
- if data.respond_to?(:[])
318
- data = data[attr]
319
- elsif data.nil?
320
- nil # don't get no method error on nil
321
- else data.respond_to?(attr.to_sym)
322
- data = data.send(attr.to_sym)
323
- end
324
- end
325
- { config[:attribute] => data.kind_of?(Chef::Node::Attribute) ? data.to_hash : data }
326
- elsif config[:run_list]
327
- data = data.run_list.run_list
328
- { "run_list" => data }
329
- elsif config[:id_only]
330
- data.respond_to?(:name) ? data.name : data["id"]
331
- else
332
- data
333
- end
334
- end
335
-
336
- def edit_data(data, parse_output=true)
337
- output = Chef::JSONCompat.to_json_pretty(data)
338
-
339
- if (!config[:no_editor])
340
- filename = "knife-edit-"
341
- 0.upto(20) { filename += rand(9).to_s }
342
- filename << ".js"
343
- filename = File.join(Dir.tmpdir, filename)
344
- tf = File.open(filename, "w")
345
- tf.sync = true
346
- tf.puts output
347
- tf.close
348
- raise "Please set EDITOR environment variable" unless system("#{config[:editor]} #{tf.path}")
349
- tf = File.open(filename, "r")
350
- output = tf.gets(nil)
351
- tf.close
352
- File.unlink(filename)
353
- end
354
-
355
- parse_output ? Chef::JSONCompat.from_json(output) : output
356
- end
357
-
358
- def confirm(question, append_instructions=true)
359
- return true if config[:yes]
360
-
361
- stdout.print question
362
- stdout.print "? (Y/N) " if append_instructions
363
- answer = stdin.readline
364
- answer.chomp!
365
- case answer
366
- when "Y", "y"
367
- true
368
- when "N", "n"
369
- self.msg("You said no, so I'm done here.")
370
- exit 3
371
- else
372
- self.msg("I have no idea what to do with #{answer}")
373
- self.msg("Just say Y or N, please.")
374
- confirm(question)
375
- end
376
- end
377
309
 
378
310
  def show_usage
379
311
  stdout.puts("USAGE: " + self.opt_parser.to_s)
@@ -387,6 +319,8 @@ class Chef
387
319
  relative_path = "nodes"
388
320
  elsif klass == Chef::DataBagItem
389
321
  relative_path = "data_bags/#{bag}"
322
+ elsif klass == Chef::Environment
323
+ relative_path = "environments"
390
324
  end
391
325
 
392
326
  relative_file = File.expand_path(File.join(Dir.pwd, relative_path, from_file))
@@ -394,10 +328,10 @@ class Chef
394
328
 
395
329
  if file_exists_and_is_readable?(from_file)
396
330
  filename = from_file
397
- elsif file_exists_and_is_readable?(relative_file)
398
- filename = relative_file
331
+ elsif file_exists_and_is_readable?(relative_file)
332
+ filename = relative_file
399
333
  else
400
- Chef::Log.fatal("Cannot find file #{from_file}")
334
+ ui.fatal("Cannot find file #{from_file}")
401
335
  exit 30
402
336
  end
403
337
 
@@ -409,7 +343,7 @@ class Chef
409
343
  r.from_file(filename)
410
344
  r
411
345
  else
412
- Chef::Log.fatal("File must end in .js, .json, or .rb")
346
+ ui.fatal("File must end in .js, .json, or .rb")
413
347
  exit 30
414
348
  end
415
349
  end
@@ -418,33 +352,6 @@ class Chef
418
352
  File.exists?(file) && File.readable?(file)
419
353
  end
420
354
 
421
- def edit_object(klass, name)
422
- object = klass.load(name)
423
-
424
- output = edit_data(object)
425
-
426
- # Only make the save if the user changed the object.
427
- #
428
- # Output JSON for the original (object) and edited (output), then parse
429
- # them without reconstituting the objects into real classes
430
- # (create_additions=false). Then, compare the resulting simple objects,
431
- # which will be Array/Hash/String/etc.
432
- #
433
- # We wouldn't have to do these shenanigans if all the editable objects
434
- # implemented to_hash, or if to_json against a hash returned a string
435
- # with stable key order.
436
- object_parsed_again = Chef::JSONCompat.from_json(Chef::JSONCompat.to_json(object), :create_additions => false)
437
- output_parsed_again = Chef::JSONCompat.from_json(Chef::JSONCompat.to_json(output), :create_additions => false)
438
- if object_parsed_again != output_parsed_again
439
- output.save
440
- self.msg("Saved #{output}")
441
- else
442
- self.msg("Object unchanged, not saving")
443
- end
444
-
445
- output(format_for_display(object)) if config[:print_after]
446
- end
447
-
448
355
  def create_object(object, pretty_name=nil, &block)
449
356
  output = edit_data(object)
450
357
 
@@ -457,7 +364,7 @@ class Chef
457
364
  pretty_name ||= output
458
365
 
459
366
  self.msg("Created (or updated) #{pretty_name}")
460
-
367
+
461
368
  output(output) if config[:print_after]
462
369
  end
463
370
 
@@ -505,18 +412,6 @@ class Chef
505
412
  end
506
413
  end
507
414
 
508
- def msg(message)
509
- stdout.puts message
510
- end
511
-
512
- def stdout
513
- STDOUT
514
- end
515
-
516
- def stdin
517
- STDIN
518
- end
519
-
520
415
  def rest
521
416
  @rest ||= Chef::REST.new(Chef::Config[:chef_server_url])
522
417
  end