chef-vault 2.4.0 → 2.5.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 (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