chef-vault 2.4.0 → 2.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -0
  3. data/.rubocop_todo.yml +97 -0
  4. data/Changelog.md +34 -0
  5. data/DEMO.md +13 -6
  6. data/README.md +9 -4
  7. data/Rakefile +47 -10
  8. data/THEORY.md +361 -0
  9. data/bin/chef-vault +5 -5
  10. data/chef-vault.gemspec +11 -2
  11. data/features/clean_unknown_clients.feature +28 -3
  12. data/features/step_definitions/chef-databag.rb +5 -0
  13. data/features/step_definitions/chef-repo.rb +46 -8
  14. data/features/step_definitions/chef-vault.rb +69 -15
  15. data/features/support/env.rb +2 -2
  16. data/features/vault_create.feature +54 -0
  17. data/features/vault_list.feature +26 -0
  18. data/features/vault_show.feature +46 -0
  19. data/features/vault_update.feature +17 -0
  20. data/features/wrong_private_key.feature +14 -0
  21. data/lib/chef-vault.rb +0 -1
  22. data/lib/chef-vault/certificate.rb +1 -1
  23. data/lib/chef-vault/chef_patch/api_client.rb +1 -1
  24. data/lib/chef-vault/chef_patch/user.rb +1 -1
  25. data/lib/chef-vault/exceptions.rb +33 -12
  26. data/lib/chef-vault/item.rb +262 -209
  27. data/lib/chef-vault/item_keys.rb +90 -88
  28. data/lib/chef-vault/user.rb +1 -1
  29. data/lib/chef-vault/version.rb +2 -2
  30. data/lib/chef/knife/decrypt.rb +1 -2
  31. data/lib/chef/knife/encrypt_create.rb +1 -2
  32. data/lib/chef/knife/encrypt_delete.rb +1 -2
  33. data/lib/chef/knife/encrypt_remove.rb +1 -2
  34. data/lib/chef/knife/encrypt_rotate_keys.rb +1 -2
  35. data/lib/chef/knife/encrypt_update.rb +1 -2
  36. data/lib/chef/knife/mixin/compat.rb +3 -3
  37. data/lib/chef/knife/mixin/helper.rb +6 -8
  38. data/lib/chef/knife/vault_admins.rb +1 -2
  39. data/lib/chef/knife/vault_base.rb +2 -2
  40. data/lib/chef/knife/vault_create.rb +3 -4
  41. data/lib/chef/knife/vault_decrypt.rb +3 -4
  42. data/lib/chef/knife/vault_delete.rb +2 -4
  43. data/lib/chef/knife/vault_download.rb +1 -2
  44. data/lib/chef/knife/vault_edit.rb +3 -6
  45. data/lib/chef/knife/vault_list.rb +53 -0
  46. data/lib/chef/knife/vault_refresh.rb +1 -2
  47. data/lib/chef/knife/vault_remove.rb +3 -7
  48. data/lib/chef/knife/vault_rotate_all_keys.rb +2 -4
  49. data/lib/chef/knife/vault_rotate_keys.rb +2 -4
  50. data/lib/chef/knife/vault_show.rb +4 -5
  51. data/lib/chef/knife/vault_update.rb +7 -9
  52. data/spec/chef-vault/certificate_spec.rb +0 -2
  53. data/spec/chef-vault/item_spec.rb +77 -1
  54. data/spec/chef-vault/user_spec.rb +0 -2
  55. data/spec/chef-vault_spec.rb +1 -1
  56. data/spec/spec_helper.rb +1 -3
  57. metadata +38 -14
@@ -0,0 +1,14 @@
1
+ Feature: Wrong private key during decrypt
2
+
3
+ https://github.com/Nordstrom/chef-vault/issues/43
4
+ If a vault is encrypted for a node and then the node's private
5
+ key is regenerated, the error that comes back from chef-vault
6
+ should be informative, not a lower-level error from OpenSSL
7
+ like 'OpenSSL::PKey::RSAError: padding check failed'
8
+
9
+ Scenario: Regenerate node key and attempt decrypt
10
+ Given a local mode chef repo with nodes 'one,two'
11
+ And I create a vault item 'test/item' containing the JSON '{"foo": "bar"}' encrypted for 'one,two'
12
+ And I regenerate the client key for the node 'one'
13
+ And I try to decrypt the vault item 'test/item' as 'one'
14
+ Then the output should match /is encrypted for you, but your private key failed to decrypt the contents/
@@ -28,7 +28,6 @@ require 'chef-vault/chef_patch/api_client'
28
28
  require 'chef-vault/chef_patch/user'
29
29
 
30
30
  class ChefVault
31
-
32
31
  attr_accessor :vault
33
32
 
34
33
  def initialize(vault, chef_config_file=nil)
@@ -1,5 +1,5 @@
1
1
  # Description: ChefVault::Certificate class
2
- # Copyright 2013, Nordstrom, Inc.
2
+ # Copyright 2013-15, Nordstrom, Inc.
3
3
 
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
5
5
  # you may not use this file except in compliance with the License.
@@ -21,7 +21,7 @@ class ChefVault
21
21
  # the private key for Chef 10
22
22
  def self.load(name)
23
23
  response = http_api.get("clients/#{name}")
24
- if response.kind_of?(Chef::ApiClient)
24
+ if response.is_a?(Chef::ApiClient)
25
25
  response
26
26
  else
27
27
  client = Chef::ApiClient.new
@@ -30,4 +30,4 @@ class ChefVault
30
30
  end
31
31
  end
32
32
  end
33
- end
33
+ end
@@ -1,5 +1,5 @@
1
1
  # Author:: Kevin Moser <kevin.moser@nordstrom.com>
2
- # Copyright:: Copyright 2013, Nordstrom, Inc.
2
+ # Copyright:: Copyright 2013-15, Nordstrom, Inc.
3
3
  # License:: Apache License, Version 2.0
4
4
 
5
5
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -14,15 +14,36 @@
14
14
  # See the License for the specific language governing permissions and
15
15
  # limitations under the License.
16
16
 
17
- class ChefVault::Exceptions
18
- class SecretDecryption < RuntimeError; end
19
- class NoKeysDefined < RuntimeError; end
20
- class ItemNotEncrypted < RuntimeError; end
21
- class KeysActionNotValue < RuntimeError; end
22
- class AdminNotFound < RuntimeError; end
23
- class ClientNotFound < RuntimeError; end
24
- class KeysNotFound < RuntimeError; end
25
- class ItemNotFound < RuntimeError; end
26
- class ItemAlreadyExists < RuntimeError; end
27
- class SearchNotFound < RuntimeError; end
17
+ class ChefVault
18
+ class Exceptions < RuntimeError
19
+ class SecretDecryption < Exceptions
20
+ end
21
+
22
+ class NoKeysDefined < Exceptions
23
+ end
24
+
25
+ class ItemNotEncrypted < Exceptions
26
+ end
27
+
28
+ class KeysActionNotValue < Exceptions
29
+ end
30
+
31
+ class AdminNotFound < Exceptions
32
+ end
33
+
34
+ class ClientNotFound < Exceptions
35
+ end
36
+
37
+ class KeysNotFound < Exceptions
38
+ end
39
+
40
+ class ItemNotFound < Exceptions
41
+ end
42
+
43
+ class ItemAlreadyExists < Exceptions
44
+ end
45
+
46
+ class SearchNotFound < Exceptions
47
+ end
48
+ end
28
49
  end
@@ -1,5 +1,5 @@
1
1
  # Author:: Kevin Moser <kevin.moser@nordstrom.com>
2
- # Copyright:: Copyright 2013, Nordstrom, Inc.
2
+ # Copyright:: Copyright 2013-15, Nordstrom, Inc.
3
3
  # License:: Apache License, Version 2.0
4
4
 
5
5
  # Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,276 +16,329 @@
16
16
 
17
17
  require 'securerandom'
18
18
 
19
- class ChefVault::Item < Chef::DataBagItem
20
- attr_accessor :keys
21
- attr_accessor :encrypted_data_bag_item
22
-
23
- def initialize(vault, name)
24
- super() # Don't pass parameters
25
- @data_bag = vault
26
- @raw_data["id"] = name
27
- @keys = ChefVault::ItemKeys.new(vault, "#{name}_keys")
28
- @secret = generate_secret
29
- @encrypted = false
30
- end
19
+ class ChefVault
20
+ class Item < Chef::DataBagItem
21
+ attr_accessor :keys
22
+ attr_accessor :encrypted_data_bag_item
23
+
24
+ def initialize(vault, name)
25
+ super() # Don't pass parameters
26
+ @data_bag = vault
27
+ @raw_data["id"] = name
28
+ @keys = ChefVault::ItemKeys.new(vault, "#{name}_keys")
29
+ @secret = generate_secret
30
+ @encrypted = false
31
+ end
31
32
 
32
- def load_keys(vault, keys)
33
- @keys = ChefVault::ItemKeys.load(vault, keys)
34
- @secret = secret
35
- end
33
+ def load_keys(vault, keys)
34
+ @keys = ChefVault::ItemKeys.load(vault, keys)
35
+ @secret = secret
36
+ end
36
37
 
37
- def clients(search=nil, action=:add)
38
- if search
39
- results_returned = false
38
+ def clients(search=nil, action=:add)
39
+ if search
40
+ results_returned = false
41
+
42
+ query = Chef::Search::Query.new
43
+ query.search(:node, search)[0].each do |node|
44
+ results_returned = true
45
+
46
+ case action
47
+ when :add
48
+ begin
49
+ keys.add(load_client(node.name), @secret, "clients")
50
+ rescue ChefVault::Exceptions::ClientNotFound
51
+ $stderr.puts "node '#{node.name}' has no private key; skipping"
52
+ end
53
+ when :delete
54
+ keys.delete(node.name, "clients")
55
+ else
56
+ raise ChefVault::Exceptions::KeysActionNotValid,
57
+ "#{action} is not a valid action"
58
+ end
59
+ end
40
60
 
41
- query = Chef::Search::Query.new
42
- query.search(:node, search)[0].each do |node|
43
- results_returned = true
44
-
45
- case action
46
- when :add
47
- keys.add(load_client(node.name), @secret, "clients")
48
- when :delete
49
- keys.delete(node.name, "clients")
50
- else
51
- raise ChefVault::Exceptions::KeysActionNotValid,
52
- "#{action} is not a valid action"
61
+ unless results_returned
62
+ puts "WARNING: No clients were returned from search, you may not have "\
63
+ "got what you expected!!"
53
64
  end
65
+ else
66
+ keys.clients
54
67
  end
68
+ end
55
69
 
56
- unless results_returned
57
- puts "WARNING: No clients were returned from search, you may not have "\
58
- "got what you expected!!"
70
+ def search(search_query=nil)
71
+ if search_query
72
+ keys.search_query(search_query)
73
+ else
74
+ keys.search_query
59
75
  end
60
- else
61
- keys.clients
62
76
  end
63
- end
64
77
 
65
- def search(search_query=nil)
66
- if search_query
67
- keys.search_query(search_query)
68
- else
69
- keys.search_query
70
- end
71
- end
72
-
73
- def admins(admins=nil, action=:add)
74
- if admins
75
- admins.split(",").each do |admin|
76
- admin.strip!
77
- case action
78
- when :add
79
- keys.add(load_admin(admin), @secret, "admins")
80
- when :delete
81
- keys.delete(admin, "admins")
82
- else
83
- raise ChefVault::Exceptions::KeysActionNotValid,
84
- "#{action} is not a valid action"
78
+ def admins(admins=nil, action=:add)
79
+ if admins
80
+ admins.split(",").each do |admin|
81
+ admin.strip!
82
+ case action
83
+ when :add
84
+ keys.add(load_admin(admin), @secret, "admins")
85
+ when :delete
86
+ keys.delete(admin, "admins")
87
+ else
88
+ raise ChefVault::Exceptions::KeysActionNotValid,
89
+ "#{action} is not a valid action"
90
+ end
85
91
  end
92
+ else
93
+ keys.admins
86
94
  end
87
- else
88
- keys.admins
89
95
  end
90
- end
91
-
92
- def remove(key)
93
- @raw_data.delete(key)
94
- end
95
96
 
96
- def secret
97
- if @keys.include?(Chef::Config[:node_name])
98
- private_key = OpenSSL::PKey::RSA.new(open(Chef::Config[:client_key]).read())
99
- private_key.private_decrypt(Base64.decode64(@keys[Chef::Config[:node_name]]))
100
- else
101
- raise ChefVault::Exceptions::SecretDecryption,
102
- "#{data_bag}/#{id} is not encrypted with your public key. "\
103
- "Contact an administrator of the vault item to encrypt for you!"
97
+ def remove(key)
98
+ @raw_data.delete(key)
104
99
  end
105
- end
106
-
107
- def rotate_keys!(clean_unknown_clients=false)
108
- @secret = generate_secret
109
100
 
110
- unless clients.empty?
111
- if clean_unknown_clients
112
- clients_to_remove=[]
113
- clients.each do |client|
114
- begin
115
- clients("name:#{client}")
116
- rescue ChefVault::Exceptions::ClientNotFound
117
- clients_to_remove.push(client)
118
- end
119
- end
120
- clients_to_remove.each do |client|
121
- puts "Removing unknown client '#{client}'"
122
- clients("name:#{client}", :delete)
101
+ def secret
102
+ if @keys.include?(Chef::Config[:node_name])
103
+ private_key = OpenSSL::PKey::RSA.new(open(Chef::Config[:client_key]).read())
104
+ begin
105
+ private_key.private_decrypt(Base64.decode64(@keys[Chef::Config[:node_name]]))
106
+ rescue OpenSSL::PKey::RSAError
107
+ raise ChefVault::Exceptions::SecretDecryption,
108
+ "#{data_bag}/#{id} is encrypted for you, but your private key failed to decrypt the contents. "\
109
+ "(if you regenerated your client key, have an administrator of the vault run 'knife vault refresh')"
123
110
  end
124
111
  else
112
+ raise ChefVault::Exceptions::SecretDecryption,
113
+ "#{data_bag}/#{id} is not encrypted with your public key. "\
114
+ "Contact an administrator of the vault item to encrypt for you!"
115
+ end
116
+ end
117
+
118
+ def rotate_keys!(clean_unknown_clients = false)
119
+ @secret = generate_secret
120
+
121
+ unless clients.empty?
122
+ # a bit of a misnomer; this doesn't remove unknown
123
+ # admins, just clients which are nodes
124
+ remove_unknown_nodes if clean_unknown_clients
125
+ # re-encrypt the new shared secret for all remaining clients
125
126
  clients.each do |client|
126
127
  clients("name:#{client}")
127
128
  end
128
129
  end
129
- end
130
130
 
131
- unless admins.empty?
132
- admins.each do |admin|
133
- admins(admin)
131
+ unless admins.empty?
132
+ # re-encrypt the new shared secret for all admins
133
+ admins.each do |admin|
134
+ admins(admin)
135
+ end
134
136
  end
137
+
138
+ save
139
+ reload_raw_data
135
140
  end
136
141
 
137
- save
138
- reload_raw_data
139
- end
142
+ def generate_secret(key_size=32)
143
+ # Defaults to 32 bytes, as this is the size that a Chef
144
+ # Encrypted Data Bag Item will digest all secrets down to anyway
145
+ SecureRandom.random_bytes(key_size)
146
+ end
140
147
 
141
- def generate_secret(key_size=32)
142
- # Defaults to 32 bytes, as this is the size that a Chef
143
- # Encrypted Data Bag Item will digest all secrets down to anyway
144
- SecureRandom.random_bytes(key_size)
145
- end
148
+ def []=(key, value)
149
+ reload_raw_data if @encrypted
150
+ super
151
+ end
146
152
 
147
- def []=(key, value)
148
- reload_raw_data if @encrypted
149
- super
150
- end
153
+ def [](key)
154
+ reload_raw_data if @encrypted
155
+ super
156
+ end
151
157
 
152
- def [](key)
153
- reload_raw_data if @encrypted
154
- super
155
- end
158
+ def save(item_id=@raw_data['id'])
159
+ # validate the format of the id before attempting to save
160
+ validate_id!(item_id)
161
+
162
+ # save the keys first, raising an error if no keys were defined
163
+ if keys.admins.empty? && keys.clients.empty?
164
+ raise ChefVault::Exceptions::NoKeysDefined,
165
+ "No keys defined for #{item_id}"
166
+ end
167
+
168
+ keys.save
156
169
 
157
- def save(item_id=@raw_data['id'])
170
+ # Make sure the item is encrypted before saving
171
+ encrypt! unless @encrypted
172
+
173
+ # Now save the encrypted data
174
+ if Chef::Config[:solo]
175
+ data_bag_path = File.join(Chef::Config[:data_bag_path],
176
+ data_bag)
177
+ data_bag_item_path = File.join(data_bag_path, item_id)
178
+
179
+ FileUtils.mkdir(data_bag_path) unless File.exist?(data_bag_path)
180
+ File.open("#{data_bag_item_path}.json", 'w') do |file|
181
+ file.write(JSON.pretty_generate(raw_data))
182
+ end
183
+
184
+ raw_data
185
+ else
186
+ begin
187
+ Chef::DataBag.load(data_bag)
188
+ rescue Net::HTTPServerException => http_error
189
+ if http_error.response.code == "404"
190
+ chef_data_bag = Chef::DataBag.new
191
+ chef_data_bag.name data_bag
192
+ chef_data_bag.create
193
+ end
194
+ end
158
195
 
159
- # validate the format of the id before attempting to save
160
- validate_id!(item_id)
196
+ super
197
+ end
198
+ end
161
199
 
162
- # save the keys first, raising an error if no keys were defined
163
- if keys.admins.empty? && keys.clients.empty?
164
- raise ChefVault::Exceptions::NoKeysDefined,
165
- "No keys defined for #{item_id}"
200
+ def to_json(*a)
201
+ json = super
202
+ json.gsub(self.class.name, self.class.superclass.name)
166
203
  end
167
204
 
168
- keys.save
205
+ def destroy
206
+ keys.destroy
169
207
 
170
- # Make sure the item is encrypted before saving
171
- encrypt! unless @encrypted
208
+ if Chef::Config[:solo]
209
+ data_bag_path = File.join(Chef::Config[:data_bag_path],
210
+ data_bag)
211
+ data_bag_item_path = File.join(data_bag_path, @raw_data["id"])
172
212
 
173
- # Now save the encrypted data
174
- if Chef::Config[:solo]
175
- data_bag_path = File.join(Chef::Config[:data_bag_path],
176
- data_bag)
177
- data_bag_item_path = File.join(data_bag_path, item_id)
213
+ FileUtils.rm("#{data_bag_item_path}.json")
178
214
 
179
- FileUtils.mkdir(data_bag_path) unless File.exists?(data_bag_path)
180
- File.open("#{data_bag_item_path}.json",'w') do |file|
181
- file.write(JSON.pretty_generate(self.raw_data))
215
+ nil
216
+ else
217
+ super(data_bag, id)
182
218
  end
219
+ end
220
+
221
+ def self.load(vault, name)
222
+ item = new(vault, name)
223
+ item.load_keys(vault, "#{name}_keys")
183
224
 
184
- self.raw_data
185
- else
186
225
  begin
187
- chef_data_bag = Chef::DataBag.load(data_bag)
226
+ item.raw_data =
227
+ Chef::EncryptedDataBagItem.load(vault, name, item.secret).to_hash
188
228
  rescue Net::HTTPServerException => http_error
189
229
  if http_error.response.code == "404"
190
- chef_data_bag = Chef::DataBag.new
191
- chef_data_bag.name data_bag
192
- chef_data_bag.create
230
+ raise ChefVault::Exceptions::ItemNotFound,
231
+ "#{vault}/#{name} could not be found"
232
+ else
233
+ raise http_error
193
234
  end
235
+ rescue Chef::Exceptions::ValidationFailed
236
+ raise ChefVault::Exceptions::ItemNotFound,
237
+ "#{vault}/#{name} could not be found"
194
238
  end
195
239
 
196
- super
240
+ item
197
241
  end
198
- end
199
-
200
- def to_json(*a)
201
- json = super
202
- json.gsub(self.class.name, self.class.superclass.name)
203
- end
204
242
 
205
- def destroy
206
- keys.destroy
243
+ private
207
244
 
208
- if Chef::Config[:solo]
209
- data_bag_path = File.join(Chef::Config[:data_bag_path],
210
- data_bag)
211
- data_bag_item_path = File.join(data_bag_path, @raw_data["id"])
212
-
213
- FileUtils.rm("#{data_bag_item_path}.json")
214
-
215
- nil
216
- else
217
- super(data_bag, id)
245
+ def encrypt!
246
+ @raw_data = Chef::EncryptedDataBagItem.encrypt_data_bag_item(self, @secret)
247
+ @encrypted = true
218
248
  end
219
- end
220
249
 
221
- def self.load(vault, name)
222
- item = new(vault, name)
223
- item.load_keys(vault, "#{name}_keys")
250
+ def reload_raw_data
251
+ @raw_data =
252
+ Chef::EncryptedDataBagItem.load(@data_bag, @raw_data["id"], secret).to_hash
253
+ @encrypted = false
224
254
 
225
- begin
226
- item.raw_data =
227
- Chef::EncryptedDataBagItem.load(vault, name, item.secret).to_hash
228
- rescue Net::HTTPServerException => http_error
229
- if http_error.response.code == "404"
230
- raise ChefVault::Exceptions::ItemNotFound,
231
- "#{vault}/#{name} could not be found"
232
- else
233
- raise http_error
234
- end
235
- rescue Chef::Exceptions::ValidationFailed
236
- raise ChefVault::Exceptions::ItemNotFound,
237
- "#{vault}/#{name} could not be found"
255
+ @raw_data
238
256
  end
239
257
 
240
- item
241
- end
258
+ def load_admin(admin)
259
+ begin
260
+ admin = ChefVault::ChefPatch::User.load(admin)
261
+ rescue Net::HTTPServerException => http_error
262
+ if http_error.response.code == "404"
263
+ begin
264
+ puts "WARNING: #{admin} not found in users, trying clients."
265
+ admin = load_client(admin)
266
+ rescue ChefVault::Exceptions::ClientNotFound
267
+ raise ChefVault::Exceptions::AdminNotFound,
268
+ "FATAL: Could not find #{admin} in users or clients!"
269
+ end
270
+ else
271
+ raise http_error
272
+ end
273
+ end
242
274
 
243
- private
244
- def encrypt!
245
- @raw_data = Chef::EncryptedDataBagItem.encrypt_data_bag_item(self, @secret)
246
- @encrypted = true
247
- end
275
+ admin
276
+ end
248
277
 
249
- def reload_raw_data
250
- @raw_data =
251
- Chef::EncryptedDataBagItem.load(@data_bag, @raw_data["id"], secret).to_hash
252
- @encrypted = false
278
+ def load_client(client)
279
+ begin
280
+ client = ChefVault::ChefPatch::ApiClient.load(client)
281
+ rescue Net::HTTPServerException => http_error
282
+ if http_error.response.code == "404"
283
+ raise ChefVault::Exceptions::ClientNotFound,
284
+ "#{client} is not a valid chef client and/or node"
285
+ else
286
+ raise http_error
287
+ end
288
+ end
253
289
 
254
- @raw_data
255
- end
290
+ client
291
+ end
256
292
 
257
- def load_admin(admin)
258
- begin
259
- admin = ChefVault::ChefPatch::User.load(admin)
260
- rescue Net::HTTPServerException => http_error
261
- if http_error.response.code == "404"
262
- begin
263
- puts "WARNING: #{admin} not found in users, trying clients."
264
- admin = load_client(admin)
265
- rescue ChefVault::Exceptions::ClientNotFound
266
- raise ChefVault::Exceptions::AdminNotFound,
267
- "FATAL: Could not find #{admin} in users or clients!"
268
- end
269
- else
270
- raise http_error
293
+ # removes unknown nodes by performing a node search
294
+ # for each of the existing nodclientses. If the search
295
+ # returns nothing or the client cannot be loaded, then
296
+ # we remove that client from the vault
297
+ # @return [void]
298
+ def remove_unknown_nodes
299
+ # build a list of clients to remove so we don't
300
+ # mutate the clients while iterating over search results
301
+ clients_to_remove = []
302
+ clients.each do |nodename|
303
+ clients_to_remove.push(nodename) unless node_exists?(nodename)
304
+ end
305
+ # now delete any flagged clients from the keys data bag
306
+ clients_to_remove.each do |client|
307
+ puts "Removing unknown client '#{client}'"
308
+ keys.delete(client, "clients")
271
309
  end
272
310
  end
273
311
 
274
- admin
275
- end
312
+ # checks if a node exists on the Chef server by performing
313
+ # a search against the node index. If the search returns no
314
+ # results, the node does not exist. If it does return results,
315
+ # check if there is a matching client
316
+ # @param nodename [String] the name of the node
317
+ # @return [Boolean] whether the node exists or not
318
+ def node_exists?(nodename)
319
+ # the node does not exist if a search for the node with that
320
+ # name returns no results
321
+ query = Chef::Search::Query.new
322
+ numresults = query.search(:node, "name:#{nodename}")[2]
323
+ return false unless numresults > 0
324
+ # if the node search does return results, predicate node
325
+ # existence on the existence of a like-named client
326
+ client_exists?(nodename)
327
+ end
276
328
 
277
- def load_client(client)
278
- begin
279
- client = ChefVault::ChefPatch::ApiClient.load(client)
280
- rescue Net::HTTPServerException => http_error
281
- if http_error.response.code == "404"
282
- raise ChefVault::Exceptions::ClientNotFound,
283
- "#{client} is not a valid chef client and/or node"
284
- else
329
+ # checks if a client exists on the Chef server. If we get back
330
+ # a 404, the client does not exist. Any other HTTP errors are
331
+ # re-raised. Otherwise, the client exists
332
+ # @param clientname [String] the name of the client
333
+ # @return [Boolean] whether the client exists or not
334
+ def client_exists?(clientname)
335
+ begin
336
+ ChefVault::ChefPatch::ApiClient.load(clientname)
337
+ rescue Net::HTTPServerException => http_error
338
+ return false if http_error.response.code == "404"
285
339
  raise http_error
286
340
  end
341
+ true
287
342
  end
288
-
289
- client
290
343
  end
291
344
  end