chef 0.9.18 → 0.10.0.beta.0

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