chef-api 0.2.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 (48) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +20 -0
  3. data/.travis.yml +14 -0
  4. data/Gemfile +12 -0
  5. data/LICENSE +201 -0
  6. data/README.md +264 -0
  7. data/Rakefile +1 -0
  8. data/chef-api.gemspec +25 -0
  9. data/lib/chef-api/boolean.rb +6 -0
  10. data/lib/chef-api/configurable.rb +78 -0
  11. data/lib/chef-api/connection.rb +466 -0
  12. data/lib/chef-api/defaults.rb +130 -0
  13. data/lib/chef-api/error_collection.rb +44 -0
  14. data/lib/chef-api/errors.rb +35 -0
  15. data/lib/chef-api/logger.rb +160 -0
  16. data/lib/chef-api/proxy.rb +72 -0
  17. data/lib/chef-api/resource.rb +16 -0
  18. data/lib/chef-api/resources/base.rb +951 -0
  19. data/lib/chef-api/resources/client.rb +85 -0
  20. data/lib/chef-api/resources/collection_proxy.rb +217 -0
  21. data/lib/chef-api/resources/cookbook.rb +24 -0
  22. data/lib/chef-api/resources/cookbook_version.rb +23 -0
  23. data/lib/chef-api/resources/data_bag.rb +136 -0
  24. data/lib/chef-api/resources/data_bag_item.rb +35 -0
  25. data/lib/chef-api/resources/environment.rb +16 -0
  26. data/lib/chef-api/resources/node.rb +17 -0
  27. data/lib/chef-api/resources/principal.rb +11 -0
  28. data/lib/chef-api/resources/role.rb +16 -0
  29. data/lib/chef-api/resources/user.rb +11 -0
  30. data/lib/chef-api/schema.rb +112 -0
  31. data/lib/chef-api/util.rb +119 -0
  32. data/lib/chef-api/validator.rb +16 -0
  33. data/lib/chef-api/validators/base.rb +82 -0
  34. data/lib/chef-api/validators/required.rb +11 -0
  35. data/lib/chef-api/validators/type.rb +23 -0
  36. data/lib/chef-api/version.rb +3 -0
  37. data/lib/chef-api.rb +76 -0
  38. data/locales/en.yml +89 -0
  39. data/spec/integration/resources/client_spec.rb +8 -0
  40. data/spec/integration/resources/environment_spec.rb +8 -0
  41. data/spec/integration/resources/node_spec.rb +8 -0
  42. data/spec/integration/resources/role_spec.rb +8 -0
  43. data/spec/spec_helper.rb +26 -0
  44. data/spec/support/chef_server.rb +115 -0
  45. data/spec/support/shared/chef_api_resource.rb +91 -0
  46. data/spec/unit/resources/base_spec.rb +47 -0
  47. data/spec/unit/resources/client_spec.rb +69 -0
  48. metadata +128 -0
@@ -0,0 +1,466 @@
1
+ require 'net/http'
2
+ require 'net/https'
3
+ require 'openssl'
4
+ require 'uri'
5
+
6
+ module ChefAPI
7
+ #
8
+ # Connection object for the ChefAPI API.
9
+ #
10
+ # @see http://docs.opscode.com/api_chef_server.html
11
+ #
12
+ class Connection
13
+ class << self
14
+ #
15
+ # @private
16
+ #
17
+ # @macro proxy
18
+ # @method $1
19
+ # Get a proxied collection for +$1+. The proxy automatically injects
20
+ # the current connection into the $2, providing a very Rubyesque way
21
+ # for handling multiple connection objects.
22
+ #
23
+ # @example Get the $1 from the connection object
24
+ # connection = ChefAPI::Connection.new('...')
25
+ # connection.$1 #=> $2 (with the connection object pre-populated)
26
+ #
27
+ # @return [ChefAPI::Proxy<$2>]
28
+ # a collection proxy for the $2
29
+ #
30
+ def proxy(name, klass)
31
+ class_eval <<-EOH, __FILE__, __LINE__ + 1
32
+ def #{name}
33
+ @#{name} ||= ChefAPI::Proxy.new(self, #{klass})
34
+ end
35
+ EOH
36
+ end
37
+ end
38
+
39
+ include ChefAPI::Configurable
40
+ include ChefAPI::Logger
41
+
42
+ #
43
+ # Create a new ChefAPI Connection with the given options. Any options
44
+ # given take precedence over the default options.
45
+ #
46
+ # @return [ChefAPI::Connection]
47
+ #
48
+ def initialize(options = {})
49
+ # Use any options given, but fall back to the defaults set on the module
50
+ ChefAPI::Configurable.keys.each do |key|
51
+ value = if options[key].nil?
52
+ ChefAPI.instance_variable_get(:"@#{key}")
53
+ else
54
+ options[key]
55
+ end
56
+
57
+ instance_variable_set(:"@#{key}", value)
58
+ end
59
+ end
60
+
61
+ #
62
+ # Determine if the given options are the same as ours.
63
+ #
64
+ # @return [Boolean]
65
+ #
66
+ def same_options?(opts)
67
+ opts.hash == options.hash
68
+ end
69
+
70
+ #
71
+ # Make a HTTP GET request
72
+ #
73
+ # @param path (see Connection#request)
74
+ # @param [Hash] params
75
+ # the list of query params
76
+ #
77
+ # @raise (see Connection#request)
78
+ # @return (see Connection#request)
79
+ #
80
+ def get(path, params = {})
81
+ request(:get, path, params)
82
+ end
83
+
84
+ #
85
+ # Make a HTTP POST request
86
+ #
87
+ # @param path (see Connection#request)
88
+ # @param [String, #read] data
89
+ # the body to use for the request
90
+ #
91
+ # @raise (see Connection#request)
92
+ # @return (see Connection#request)
93
+ #
94
+ def post(path, data)
95
+ request(:post, path, data)
96
+ end
97
+
98
+ #
99
+ # Make a HTTP PUT request
100
+ #
101
+ # @param path (see Connection#request)
102
+ # @param data (see Connection#post)
103
+ #
104
+ # @raise (see Connection#request)
105
+ # @return (see Connection#request)
106
+ #
107
+ def put(path, data)
108
+ request(:put, path, data)
109
+ end
110
+
111
+ #
112
+ # Make a HTTP PATCH request
113
+ #
114
+ # @param path (see Connection#request)
115
+ # @param data (see Connection#post)
116
+ #
117
+ # @raise (see Connection#request)
118
+ # @return (see Connection#request)
119
+ #
120
+ def patch(path, data)
121
+ request(:patch, path, data)
122
+ end
123
+
124
+ #
125
+ # Make a HTTP DELETE request
126
+ #
127
+ # @param path (see Connection#request)
128
+ # @param params (see Connection#get)
129
+ #
130
+ # @raise (see Connection#request)
131
+ # @return (see Connection#request)
132
+ #
133
+ def delete(path, params = {})
134
+ request(:delete, path, params)
135
+ end
136
+
137
+ #
138
+ # Make an HTTP request with the given verb, data, params, and headers. If
139
+ # the response has a return type of JSON, the JSON is automatically parsed
140
+ # and returned as a hash; otherwise it is returned as a string.
141
+ #
142
+ # @raise [Error::HTTPError]
143
+ # if the request is not an HTTP 200 OK
144
+ #
145
+ # @param [Symbol] verb
146
+ # the lowercase symbol of the HTTP verb (e.g. :get, :delete)
147
+ # @param [String] path
148
+ # the absolute or relative path from {Defaults.endpoint} to make the
149
+ # request against
150
+ # @param [#read, Hash, nil] data
151
+ # the data to use (varies based on the +verb+)
152
+ # @param [Hash] headers
153
+ # the list of headers to use
154
+ #
155
+ # @return [String, Hash]
156
+ # the response body
157
+ #
158
+ def request(verb, path, data = {})
159
+ log.info "===> #{verb.to_s.upcase} #{path}..."
160
+
161
+ # Build the URI and request object from the given information
162
+ uri = build_uri(verb, path, data)
163
+ request = class_for_request(verb).new(uri.request_uri)
164
+
165
+ # Add request headers
166
+ add_request_headers(request)
167
+
168
+ # Setup PATCH/POST/PUT
169
+ if [:patch, :post, :put].include?(verb)
170
+ if data.respond_to?(:read)
171
+ request.body_stream = data
172
+ elsif data.is_a?(Hash)
173
+ request.form_data = data
174
+ else
175
+ request.body = data
176
+ end
177
+ end
178
+
179
+ # Sign the request
180
+ add_signing_headers(verb, uri, request, parsed_key)
181
+
182
+ # Create the HTTP connection object - since the proxy information defaults
183
+ # to +nil+, we can just pass it to the initializer method instead of doing
184
+ # crazy strange conditionals.
185
+ connection = Net::HTTP.new(uri.host, uri.port,
186
+ proxy_address, proxy_port, proxy_username, proxy_password)
187
+
188
+ # Apply SSL, if applicable
189
+ if uri.scheme == 'https'
190
+ # Turn on SSL
191
+ connection.use_ssl = true
192
+
193
+ # Custom pem files, no problem!
194
+ if ssl_pem_file
195
+ pem = File.read(ssl_pem_file)
196
+ connection.cert = OpenSSL::X509::Certificate.new(pem)
197
+ connection.key = OpenSSL::PKey::RSA.new(pem)
198
+ connection.verify_mode = OpenSSL::SSL::VERIFY_PEER
199
+ end
200
+
201
+ # Naughty, naughty, naughty! Don't blame when when someone hops in
202
+ # and executes a MITM attack!
203
+ unless ssl_verify
204
+ log.warn "===> Disabling SSL verification..."
205
+ log.warn "Neither ChefAPI nor the maintainers are responsible for " \
206
+ "damanges incurred as a result of disabling SSL verification. " \
207
+ "Please use this with extreme caution, or consider specifying " \
208
+ "a custom certificate using `config.ssl_pem_file'."
209
+ connection.verify_mode = OpenSSL::SSL::VERIFY_NONE
210
+ end
211
+ end
212
+
213
+ # Create a connection using the block form, which will ensure the socket
214
+ # is properly closed in the event of an error.
215
+ connection.start do |http|
216
+ response = http.request(request)
217
+
218
+ case response
219
+ when Net::HTTPRedirection
220
+ redirect = URI.parse(response['location'])
221
+ log.debug "===> Performing HTTP redirect to #{redirect}"
222
+ request(verb, redirect, params)
223
+ when Net::HTTPSuccess
224
+ success(response)
225
+ else
226
+ error(response)
227
+ end
228
+ end
229
+ rescue SocketError, Errno::ECONNREFUSED, EOFError
230
+ log.warn " Unable to reach the Chef Server"
231
+ raise Error::HTTPServerUnavailable.new
232
+ end
233
+
234
+ #
235
+ # Construct a URL from the given verb and path. If the request is a GET or
236
+ # DELETE request, the params are assumed to be query params are are
237
+ # converted as such using {Connection#to_query_string}.
238
+ #
239
+ # If the path is relative, it is merged with the {Defaults.endpoint}
240
+ # attribute. If the path is absolute, it is converted to a URI object and
241
+ # returned.
242
+ #
243
+ # @param [Symbol] verb
244
+ # the lowercase HTTP verb (e.g. :+get+)
245
+ # @param [String] path
246
+ # the absolute or relative HTTP path (url) to get
247
+ # @param [Hash] params
248
+ # the list of params to build the URI with (for GET and DELETE requests)
249
+ #
250
+ # @return [URI]
251
+ #
252
+ def build_uri(verb, path, params = {})
253
+ log.info "===> Building URI..."
254
+
255
+ # Add any query string parameters
256
+ if [:delete, :get].include?(verb)
257
+ log.debug " Detected verb deserves a querystring"
258
+ log.debug " Building querystring using #{params.inspect}"
259
+ path = [path, to_query_string(params)].compact.join('?')
260
+ end
261
+
262
+ # Parse the URI
263
+ uri = URI.parse(path)
264
+
265
+ # Don't merge absolute URLs
266
+ unless uri.absolute?
267
+ log.debug " Detected URI is relative"
268
+ log.debug " Appending #{endpoint} to #{path}"
269
+ uri = URI.parse(File.join(endpoint, path))
270
+ end
271
+
272
+ # Return the URI object
273
+ uri
274
+ end
275
+
276
+ #
277
+ # Helper method to get the corresponding {Net::HTTP} class from the given
278
+ # HTTP verb.
279
+ #
280
+ # @param [#to_s] verb
281
+ # the HTTP verb to create a class from
282
+ #
283
+ # @return [Class]
284
+ #
285
+ def class_for_request(verb)
286
+ Net::HTTP.const_get(verb.to_s.capitalize)
287
+ end
288
+
289
+ #
290
+ # Convert the given hash to a list of query string parameters. Each key and
291
+ # value in the hash is URI-escaped for safety.
292
+ #
293
+ # @param [Hash] hash
294
+ # the hash to create the query string from
295
+ #
296
+ # @return [String, nil]
297
+ # the query string as a string, or +nil+ if there are no params
298
+ #
299
+ def to_query_string(hash)
300
+ hash.map do |key, value|
301
+ "#{URI.escape(key.to_s)}=#{URI.escape(value.to_s)}"
302
+ end.join('&')[/.+/]
303
+ end
304
+
305
+ private
306
+
307
+ #
308
+ # Parse the given private key. Users can specify the private key as:
309
+ #
310
+ # - the path to the key on disk
311
+ # - the raw string key
312
+ # - an +OpenSSL::PKey::RSA object+
313
+ #
314
+ # Any other implementations are not supported and will likely explode.
315
+ #
316
+ # @todo
317
+ # Handle errors when the file cannot be read due to insufficient
318
+ # permissions
319
+ #
320
+ # @return [OpenSSL::PKey::RSA]
321
+ # the RSA private key as an OpenSSL object
322
+ #
323
+ def parsed_key
324
+ return @parsed_key if @parsed_key
325
+
326
+ log.info "===> Parsing private key..."
327
+
328
+ if key.nil?
329
+ log.warn " No private key given!"
330
+ raise 'No private key given!'
331
+ end
332
+
333
+ if key.is_a?(OpenSSL::PKey::RSA)
334
+ log.debug " Detected private key is an OpenSSL Ruby object"
335
+ @parsed_key = key
336
+ end
337
+
338
+ if key =~ /(.+)\.pem$/ || File.exists?(key)
339
+ log.debug " Detected private key is the path to a file"
340
+ contents = File.read(File.expand_path(key))
341
+ @parsed_key = OpenSSL::PKey::RSA.new(contents)
342
+ else
343
+ log.debug " Detected private key was the literal string key"
344
+ @parsed_key = OpenSSL::PKey::RSA.new(key)
345
+ end
346
+
347
+ @parsed_key
348
+ end
349
+
350
+ #
351
+ # Parse the response object and manipulate the result based on the given
352
+ # +Content-Type+ header. For now, this method only parses JSON, but it
353
+ # could be expanded in the future to accept other content types.
354
+ #
355
+ # @param [HTTP::Message] response
356
+ # the response object from the request
357
+ #
358
+ # @return [String, Hash]
359
+ # the parsed response, as an object
360
+ #
361
+ def success(response)
362
+ log.info "===> Parsing response as success..."
363
+
364
+ case response['Content-Type']
365
+ when 'application/json'
366
+ log.debug " Detected response as JSON"
367
+ log.debug " Parsing response body as JSON"
368
+ JSON.parse(response.body)
369
+ else
370
+ log.debug " Detected response as text/plain"
371
+ response.body
372
+ end
373
+ end
374
+
375
+ #
376
+ # Raise a response error, extracting as much information from the server's
377
+ # response as possible.
378
+ #
379
+ # @param [HTTP::Message] response
380
+ # the response object from the request
381
+ #
382
+ def error(response)
383
+ log.info "===> Parsing response as error..."
384
+
385
+ case response['Content-Type']
386
+ when 'application/json'
387
+ log.debug " Detected error response as JSON"
388
+ log.debug " Parsing error response as JSON"
389
+ message = JSON.parse(response.body)['error'].first
390
+ else
391
+ log.debug " Detected response as text/plain"
392
+ message = response.body
393
+ end
394
+
395
+ case response.code.to_i
396
+ when 400
397
+ raise Error::HTTPBadRequest.new(message: message)
398
+ when 401
399
+ raise Error::HTTPUnauthorizedRequest.new(message: message)
400
+ when 403
401
+ raise Error::HTTPForbiddenRequest.new(message: message)
402
+ when 404
403
+ raise Error::HTTPNotFound.new(message: message)
404
+ when 405
405
+ raise Error::HTTPMethodNotAllowed.new(message: message)
406
+ when 406
407
+ raise Error::HTTPNotAcceptable.new(message: message)
408
+ when 500..600
409
+ raise Error::HTTPServerUnavailable.new
410
+ else
411
+ raise "I got an error #{response.code} that I don't know how to handle!"
412
+ end
413
+ end
414
+
415
+ #
416
+ # Adds the default headers to the request object.
417
+ #
418
+ # @param [Net::HTTP::Request] request
419
+ #
420
+ def add_request_headers(request)
421
+ log.info "===> Adding request headers..."
422
+
423
+ headers = {
424
+ 'Accept' => 'application/json',
425
+ 'Content-Type' => 'application/json',
426
+ 'Connection' => 'keep-alive',
427
+ 'Keep-Alive' => '30',
428
+ 'User-Agent' => user_agent,
429
+ 'X-Chef-Version' => '11.4.0',
430
+ }
431
+
432
+ headers.each do |key, value|
433
+ log.debug " #{key}: #{value}"
434
+ request[key] = value
435
+ end
436
+ end
437
+
438
+ #
439
+ # Use mixlib-auth to create a signed header auth.
440
+ #
441
+ # @param [Net::HTTP::Request] request
442
+ #
443
+ def add_signing_headers(verb, uri, request, key)
444
+ log.info "===> Adding signed header authentication..."
445
+
446
+ unless defined?(Mixlib::Authentication)
447
+ require 'mixlib/authentication/signedheaderauth'
448
+ end
449
+
450
+ headers = Mixlib::Authentication::SignedHeaderAuth.signing_object(
451
+ :http_method => verb,
452
+ :body => request.body || '',
453
+ :host => "#{uri.host}:#{uri.port}",
454
+ :path => request.path,
455
+ :timestamp => Time.now.utc.iso8601,
456
+ :user_id => client,
457
+ :file => '',
458
+ ).sign(key)
459
+
460
+ headers.each do |key, value|
461
+ log.debug " #{key}: #{value}"
462
+ request[key] = value
463
+ end
464
+ end
465
+ end
466
+ end
@@ -0,0 +1,130 @@
1
+ require 'chef-api/version'
2
+
3
+ module ChefAPI
4
+ module Defaults
5
+ # Default API endpoint
6
+ ENDPOINT = 'http://localhost:4000/'.freeze
7
+
8
+ # Default User Agent header string
9
+ USER_AGENT = "ChefAPI Ruby Gem #{ChefAPI::VERSION}".freeze
10
+
11
+ class << self
12
+ #
13
+ # The list of calculated default options for the configuration.
14
+ #
15
+ # @return [Hash]
16
+ #
17
+ def options
18
+ Hash[Configurable.keys.map { |key| [key, send(key)] }]
19
+ end
20
+
21
+ #
22
+ # The endpoint where the Chef Server lives. This is equivalent to the
23
+ # +chef_server_url+ in Chef terminology. If you are using Enterprise
24
+ # Hosted Chef or Enterprise Chef on premise, this endpoint includes your
25
+ # organization name, such as:
26
+ #
27
+ # https://api.opscode.com/organizations/NAME
28
+ #
29
+ # If you are running Open Source Chef Server or Chef Zero, this is just
30
+ # the URL to your Chef Server instance, such as:
31
+ #
32
+ # http://chef.example.com/
33
+ #
34
+ # @return [String]
35
+ #
36
+ def endpoint
37
+ ENV['CHEF_API_ENDPOINT'] || ENDPOINT
38
+ end
39
+
40
+ #
41
+ # The User Agent header to send along.
42
+ #
43
+ # @return [String]
44
+ #
45
+ def user_agent
46
+ ENV['CHEF_API_USER_AGENT'] || USER_AGENT
47
+ end
48
+
49
+ #
50
+ # The name of the Chef API client. This is the equivalent of
51
+ # +client_name+ in Chef terminology. In most cases, this is your Chef
52
+ # username.
53
+ #
54
+ # @return [String, nil]
55
+ #
56
+ def client
57
+ ENV['CHEF_API_CLIENT']
58
+ end
59
+
60
+ #
61
+ # The private key to authentication against the Chef Server. This is
62
+ # equivalent to the +client_key+ in Chef terminology. This value can
63
+ # be the client key in plain text or a path to the key on disk.
64
+ #
65
+ # @return [String, nil]
66
+ #
67
+ def key
68
+ ENV['CHEF_API_KEY']
69
+ end
70
+ #
71
+ # The HTTP Proxy server address as a string
72
+ #
73
+ # @return [String, nil]
74
+ #
75
+ def proxy_address
76
+ ENV['CHEF_API_PROXY_ADDRESS']
77
+ end
78
+
79
+ #
80
+ # The HTTP Proxy user password as a string
81
+ #
82
+ # @return [String, nil]
83
+ #
84
+ def proxy_password
85
+ ENV['CHEF_API_PROXY_PASSWORD']
86
+ end
87
+
88
+ #
89
+ # The HTTP Proxy server port as a string
90
+ #
91
+ # @return [String, nil]
92
+ #
93
+ def proxy_port
94
+ ENV['CHEF_API_PROXY_PORT']
95
+ end
96
+
97
+ #
98
+ # The HTTP Proxy server username as a string
99
+ #
100
+ # @return [String, nil]
101
+ #
102
+ def proxy_username
103
+ ENV['CHEF_API_PROXY_USERNAME']
104
+ end
105
+
106
+ #
107
+ # The path to a pem file on disk for use with a custom SSL verification
108
+ #
109
+ # @return [String, nil]
110
+ #
111
+ def ssl_pem_file
112
+ ENV['CHEF_API_SSL_PEM_FILE']
113
+ end
114
+
115
+ #
116
+ # Verify SSL requests (default: true)
117
+ #
118
+ # @return [true, false]
119
+ #
120
+ def ssl_verify
121
+ if ENV['CHEF_API_SSL_VERIFY'].nil?
122
+ true
123
+ else
124
+ %w[t y].include?(ENV['CHEF_API_SSL_VERIFY'].downcase[0])
125
+ end
126
+ end
127
+
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,44 @@
1
+ module ChefAPI
2
+ #
3
+ # Private internal class for managing the error collection.
4
+ #
5
+ class ErrorCollection < Hash
6
+ #
7
+ # The default proc for the hash needs to be an empty Array.
8
+ #
9
+ # @return [Proc]
10
+ #
11
+ def initialize
12
+ super { |h, k| h[k] = [] }
13
+ end
14
+
15
+ #
16
+ # Add a new error to the hash.
17
+ #
18
+ # @param [Symbol] key
19
+ # the attribute key
20
+ # @param [String] error
21
+ # the error message to push
22
+ #
23
+ # @return [self]
24
+ #
25
+ def add(key, error)
26
+ self[key].push(error)
27
+ self
28
+ end
29
+
30
+ #
31
+ # Output the full messages for each error. This is useful for displaying
32
+ # information about validation to the user when something goes wrong.
33
+ #
34
+ # @return [Array<String>]
35
+ #
36
+ def full_messages
37
+ self.map do |key, errors|
38
+ errors.map do |error|
39
+ "`#{key}' #{error}"
40
+ end
41
+ end.flatten
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,35 @@
1
+ module ChefAPI
2
+ module Error
3
+ class ChefAPIError < StandardError
4
+ def initialize(options = {})
5
+ class_name = self.class.to_s.split('::').last
6
+ error_key = Util.underscore(class_name)
7
+
8
+ super I18n.t("chef_api.errors.#{error_key}", options)
9
+ end
10
+ end
11
+
12
+ class AbstractMethod < ChefAPIError; end
13
+ class CannotRegenerateKey < ChefAPIError; end
14
+ class FileNotFound < ChefAPIError; end
15
+
16
+ class HTTPError < ChefAPIError; end
17
+ class HTTPBadRequest < HTTPError; end
18
+ class HTTPForbiddenRequest < HTTPError; end
19
+ class HTTPNotAcceptable < HTTPError; end
20
+ class HTTPNotFound < HTTPError; end
21
+ class HTTPMethodNotAllowed < HTTPError; end
22
+ class HTTPServerUnavailable < HTTPError; end
23
+
24
+ class HTTPUnauthorizedRequest < ChefAPIError; end
25
+ class InsufficientFilePermissions < ChefAPIError; end
26
+ class InvalidResource < ChefAPIError; end
27
+ class InvalidValidator < ChefAPIError; end
28
+ class MissingURLParameter < ChefAPIError; end
29
+ class NotADirectory < ChefAPIError; end
30
+ class ResourceAlreadyExists < ChefAPIError; end
31
+ class ResourceNotFound < ChefAPIError; end
32
+ class ResourceNotMutable < ChefAPIError; end
33
+ class UnknownAttribute < ChefAPIError; end
34
+ end
35
+ end