chef-infra-api 0.9.1

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 (58) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +201 -0
  3. data/lib/chef-api.rb +96 -0
  4. data/lib/chef-api/aclable.rb +35 -0
  5. data/lib/chef-api/authentication.rb +300 -0
  6. data/lib/chef-api/boolean.rb +6 -0
  7. data/lib/chef-api/configurable.rb +80 -0
  8. data/lib/chef-api/connection.rb +507 -0
  9. data/lib/chef-api/defaults.rb +197 -0
  10. data/lib/chef-api/error_collection.rb +44 -0
  11. data/lib/chef-api/errors.rb +64 -0
  12. data/lib/chef-api/multipart.rb +164 -0
  13. data/lib/chef-api/resource.rb +21 -0
  14. data/lib/chef-api/resources/base.rb +960 -0
  15. data/lib/chef-api/resources/client.rb +84 -0
  16. data/lib/chef-api/resources/collection_proxy.rb +234 -0
  17. data/lib/chef-api/resources/cookbook.rb +24 -0
  18. data/lib/chef-api/resources/cookbook_version.rb +23 -0
  19. data/lib/chef-api/resources/data_bag.rb +136 -0
  20. data/lib/chef-api/resources/data_bag_item.rb +53 -0
  21. data/lib/chef-api/resources/environment.rb +16 -0
  22. data/lib/chef-api/resources/group.rb +16 -0
  23. data/lib/chef-api/resources/node.rb +20 -0
  24. data/lib/chef-api/resources/organization.rb +22 -0
  25. data/lib/chef-api/resources/partial_search.rb +44 -0
  26. data/lib/chef-api/resources/principal.rb +11 -0
  27. data/lib/chef-api/resources/role.rb +18 -0
  28. data/lib/chef-api/resources/search.rb +47 -0
  29. data/lib/chef-api/resources/user.rb +82 -0
  30. data/lib/chef-api/schema.rb +150 -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/templates/errors/abstract_method.erb +5 -0
  38. data/templates/errors/cannot_regenerate_key.erb +1 -0
  39. data/templates/errors/chef_api_error.erb +1 -0
  40. data/templates/errors/file_not_found.erb +1 -0
  41. data/templates/errors/http_bad_request.erb +3 -0
  42. data/templates/errors/http_forbidden_request.erb +3 -0
  43. data/templates/errors/http_gateway_timeout.erb +3 -0
  44. data/templates/errors/http_method_not_allowed.erb +3 -0
  45. data/templates/errors/http_not_acceptable.erb +3 -0
  46. data/templates/errors/http_not_found.erb +3 -0
  47. data/templates/errors/http_server_unavailable.erb +1 -0
  48. data/templates/errors/http_unauthorized_request.erb +3 -0
  49. data/templates/errors/insufficient_file_permissions.erb +1 -0
  50. data/templates/errors/invalid_resource.erb +1 -0
  51. data/templates/errors/invalid_validator.erb +1 -0
  52. data/templates/errors/missing_url_parameter.erb +1 -0
  53. data/templates/errors/not_a_directory.erb +1 -0
  54. data/templates/errors/resource_already_exists.erb +1 -0
  55. data/templates/errors/resource_not_found.erb +1 -0
  56. data/templates/errors/resource_not_mutable.erb +1 -0
  57. data/templates/errors/unknown_attribute.erb +1 -0
  58. metadata +130 -0
@@ -0,0 +1,197 @@
1
+ require 'chef-api/version'
2
+ require 'pathname'
3
+ require 'json'
4
+
5
+ module ChefAPI
6
+ module Defaults
7
+ # Default API endpoint
8
+ ENDPOINT = 'https://api.opscode.com/'.freeze
9
+
10
+ # Default User Agent header string
11
+ USER_AGENT = "ChefAPI Ruby Gem #{ChefAPI::VERSION}".freeze
12
+
13
+ class << self
14
+ #
15
+ # The list of calculated default options for the configuration.
16
+ #
17
+ # @return [Hash]
18
+ #
19
+ def options
20
+ Hash[Configurable.keys.map { |key| [key, send(key)] }]
21
+ end
22
+
23
+ #
24
+ # The Chef API configuration
25
+ #
26
+ # @return [Hash]
27
+ def config
28
+ path = config_path
29
+ @config ||= path.exist? ? JSON.parse(path.read) : {}
30
+ end
31
+
32
+ #
33
+ # Pathname to configuration file, or a blank Pathname.
34
+ #
35
+ # @return [Pathname] an expanded Pathname or a non-existent Pathname
36
+ def config_path
37
+ if result = chef_api_config_path
38
+ Pathname(result).expand_path
39
+ else
40
+ Pathname('')
41
+ end
42
+ end
43
+
44
+ #
45
+ # String representation of path to configuration file
46
+ #
47
+ # @return [String, nil] Path to config file, or nil
48
+ def chef_api_config_path
49
+ ENV['CHEF_API_CONFIG'] || if ENV.key?('HOME')
50
+ '~/.chef-api'
51
+ else
52
+ nil
53
+ end
54
+ end
55
+
56
+ #
57
+ # The endpoint where the Chef Server lives. This is equivalent to the
58
+ # +chef_server_url+ in Chef terminology. If you are using Enterprise
59
+ # Hosted Chef or Enterprise Chef on premise, this endpoint should include
60
+ # your organization name. For example:
61
+ #
62
+ # https://api.opscode.com/organizations/bacon
63
+ #
64
+ # If you are running Open Source Chef Server or Chef Zero, this is the
65
+ # full URL to your Chef Server instance, including the server port and
66
+ # FQDN.
67
+ #
68
+ # https://chef.server.local:4567/
69
+ #
70
+ # @return [String] (default: +https://api.opscode.com/+)
71
+ #
72
+ def endpoint
73
+ ENV['CHEF_API_ENDPOINT'] || config['CHEF_API_ENDPOINT'] || ENDPOINT
74
+ end
75
+
76
+ #
77
+ # The flavor of the target Chef Server. There are two possible values:
78
+ #
79
+ # - enterprise
80
+ # - open_source
81
+ #
82
+ # "Enterprise" covers both Hosted Chef and Enterprise Chef. "Open Source"
83
+ # covers both Chef Zero and Open Source Chef Server.
84
+ #
85
+ # @return [true, false]
86
+ #
87
+ def flavor
88
+ if ENV['CHEF_API_FLAVOR']
89
+ ENV['CHEF_API_FLAVOR'].to_sym
90
+ elsif config['CHEF_API_FLAVOR']
91
+ config['CHEF_API_FLAVOR']
92
+ else
93
+ endpoint.include?('/organizations') ? :enterprise : :open_source
94
+ end
95
+ end
96
+
97
+ #
98
+ # The User Agent header to send along.
99
+ #
100
+ # @return [String]
101
+ #
102
+ def user_agent
103
+ ENV['CHEF_API_USER_AGENT'] || config['CHEF_API_USER_AGENT'] || USER_AGENT
104
+ end
105
+
106
+ #
107
+ # The name of the Chef API client. This is the equivalent of
108
+ # +client_name+ in Chef terminology. In most cases, this is your Chef
109
+ # username.
110
+ #
111
+ # @return [String, nil]
112
+ #
113
+ def client
114
+ ENV['CHEF_API_CLIENT'] || config['CHEF_API_CLIENT']
115
+ end
116
+
117
+ #
118
+ # The private key to authentication against the Chef Server. This is
119
+ # equivalent to the +client_key+ in Chef terminology. This value can
120
+ # be the client key in plain text or a path to the key on disk.
121
+ #
122
+ # @return [String, nil]
123
+ #
124
+ def key
125
+ ENV['CHEF_API_KEY'] || config['CHEF_API_KEY']
126
+ end
127
+ #
128
+ # The HTTP Proxy server address as a string
129
+ #
130
+ # @return [String, nil]
131
+ #
132
+ def proxy_address
133
+ ENV['CHEF_API_PROXY_ADDRESS'] || config['CHEF_API_PROXY_ADDRESS']
134
+ end
135
+
136
+ #
137
+ # The HTTP Proxy user password as a string
138
+ #
139
+ # @return [String, nil]
140
+ #
141
+ def proxy_password
142
+ ENV['CHEF_API_PROXY_PASSWORD'] || config['CHEF_API_PROXY_PASSWORD']
143
+ end
144
+
145
+ #
146
+ # The HTTP Proxy server port as a string
147
+ #
148
+ # @return [String, nil]
149
+ #
150
+ def proxy_port
151
+ ENV['CHEF_API_PROXY_PORT'] || config['CHEF_API_PROXY_PORT']
152
+ end
153
+
154
+ #
155
+ # The HTTP Proxy server username as a string
156
+ #
157
+ # @return [String, nil]
158
+ #
159
+ def proxy_username
160
+ ENV['CHEF_API_PROXY_USERNAME'] || config['CHEF_API_PROXY_USERNAME']
161
+ end
162
+
163
+ #
164
+ # The path to a pem file on disk for use with a custom SSL verification
165
+ #
166
+ # @return [String, nil]
167
+ #
168
+ def ssl_pem_file
169
+ ENV['CHEF_API_SSL_PEM_FILE'] || config['CHEF_API_SSL_PEM_FILE']
170
+ end
171
+
172
+ #
173
+ # Verify SSL requests (default: true)
174
+ #
175
+ # @return [true, false]
176
+ #
177
+ def ssl_verify
178
+ if ENV['CHEF_API_SSL_VERIFY'].nil? && config['CHEF_API_SSL_VERIFY'].nil?
179
+ true
180
+ else
181
+ %w[t y].include?(ENV['CHEF_API_SSL_VERIFY'].downcase[0]) || config['CHEF_API_SSL_VERIFY']
182
+ end
183
+ end
184
+
185
+ #
186
+ # Network request read timeout in seconds (default: 60)
187
+ #
188
+ # @return [Integer, nil]
189
+ #
190
+ def read_timeout
191
+ timeout_from_env = ENV['CHEF_API_READ_TIMEOUT'] || config['CHEF_API_READ_TIMEOUT']
192
+
193
+ Integer(timeout_from_env) unless timeout_from_env.nil?
194
+ end
195
+ end
196
+ end
197
+ 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,64 @@
1
+ require 'erb'
2
+
3
+ module ChefAPI
4
+ module Error
5
+ class ErrorBinding
6
+ def initialize(options = {})
7
+ options.each do |key, value|
8
+ instance_variable_set(:"@#{key}", value)
9
+ end
10
+ end
11
+
12
+ def get_binding
13
+ binding
14
+ end
15
+ end
16
+
17
+ class ChefAPIError < StandardError
18
+ def initialize(options = {})
19
+ @options = options
20
+ @filename = options.delete(:_template)
21
+
22
+ super()
23
+ end
24
+
25
+ def message
26
+ erb = ERB.new(File.read(template))
27
+ erb.result(ErrorBinding.new(@options).get_binding)
28
+ end
29
+ alias_method :to_s, :message
30
+
31
+ private
32
+
33
+ def template
34
+ class_name = self.class.to_s.split('::').last
35
+ filename = @filename || Util.underscore(class_name)
36
+ ChefAPI.root.join('templates', 'errors', "#{filename}.erb")
37
+ end
38
+ end
39
+
40
+ class AbstractMethod < ChefAPIError; end
41
+ class CannotRegenerateKey < ChefAPIError; end
42
+ class FileNotFound < ChefAPIError; end
43
+
44
+ class HTTPError < ChefAPIError; end
45
+ class HTTPBadRequest < HTTPError; end
46
+ class HTTPForbiddenRequest < HTTPError; end
47
+ class HTTPGatewayTimeout < HTTPError; end
48
+ class HTTPNotAcceptable < HTTPError; end
49
+ class HTTPNotFound < HTTPError; end
50
+ class HTTPMethodNotAllowed < HTTPError; end
51
+ class HTTPServerUnavailable < HTTPError; end
52
+
53
+ class HTTPUnauthorizedRequest < ChefAPIError; end
54
+ class InsufficientFilePermissions < ChefAPIError; end
55
+ class InvalidResource < ChefAPIError; end
56
+ class InvalidValidator < ChefAPIError; end
57
+ class MissingURLParameter < ChefAPIError; end
58
+ class NotADirectory < ChefAPIError; end
59
+ class ResourceAlreadyExists < ChefAPIError; end
60
+ class ResourceNotFound < ChefAPIError; end
61
+ class ResourceNotMutable < ChefAPIError; end
62
+ class UnknownAttribute < ChefAPIError; end
63
+ end
64
+ end
@@ -0,0 +1,164 @@
1
+ require 'cgi'
2
+ require 'mime/types'
3
+
4
+ module ChefAPI
5
+ module Multipart
6
+ BOUNDARY = '------ChefAPIMultipartBoundary'.freeze
7
+
8
+ class Body
9
+ def initialize(params = {})
10
+ params.each do |key, value|
11
+ if value.respond_to?(:read)
12
+ parts << FilePart.new(key, value)
13
+ else
14
+ parts << ParamPart.new(key, value)
15
+ end
16
+ end
17
+
18
+ parts << EndingPart.new
19
+ end
20
+
21
+ def stream
22
+ MultiIO.new(*parts.map(&:io))
23
+ end
24
+
25
+ def content_type
26
+ "multipart/form-data; boundary=#{BOUNDARY}"
27
+ end
28
+
29
+ def content_length
30
+ parts.map(&:size).inject(:+)
31
+ end
32
+
33
+ private
34
+
35
+ def parts
36
+ @parts ||= []
37
+ end
38
+ end
39
+
40
+ class MultiIO
41
+ attr_reader :ios
42
+
43
+ def initialize(*ios)
44
+ @ios = ios
45
+ @index = 0
46
+ end
47
+
48
+ # Read from IOs in order until `length` bytes have been received.
49
+ def read(length = nil, outbuf = nil)
50
+ got_result = false
51
+ outbuf = outbuf ? outbuf.replace('') : ''
52
+
53
+ while io = current_io
54
+ if result = io.read(length)
55
+ got_result ||= !result.nil?
56
+ result.force_encoding('BINARY') if result.respond_to?(:force_encoding)
57
+ outbuf << result
58
+ length -= result.length if length
59
+ break if length == 0
60
+ end
61
+ advance_io
62
+ end
63
+
64
+ (!got_result && length) ? nil : outbuf
65
+ end
66
+
67
+ def rewind
68
+ @ios.each { |io| io.rewind }
69
+ @index = 0
70
+ end
71
+
72
+ private
73
+
74
+ def current_io
75
+ @ios[@index]
76
+ end
77
+
78
+ def advance_io
79
+ @index += 1
80
+ end
81
+ end
82
+
83
+ #
84
+ # A generic key => value part.
85
+ #
86
+ class ParamPart
87
+ def initialize(name, value)
88
+ @part = build(name, value)
89
+ end
90
+
91
+ def io
92
+ @io ||= StringIO.new(@part)
93
+ end
94
+
95
+ def size
96
+ @part.bytesize
97
+ end
98
+
99
+ private
100
+
101
+ def build(name, value)
102
+ part = %|--#{BOUNDARY}\r\n|
103
+ part << %|Content-Disposition: form-data; name="#{CGI.escape(name)}"\r\n\r\n|
104
+ part << %|#{value}\r\n|
105
+ part
106
+ end
107
+ end
108
+
109
+ #
110
+ # A File part
111
+ #
112
+ class FilePart
113
+ def initialize(name, file)
114
+ @file = file
115
+ @head = build(name, file)
116
+ @foot = "\r\n"
117
+ end
118
+
119
+ def io
120
+ @io ||= MultiIO.new(
121
+ StringIO.new(@head),
122
+ @file,
123
+ StringIO.new(@foot)
124
+ )
125
+ end
126
+
127
+ def size
128
+ @head.bytesize + @file.size + @foot.bytesize
129
+ end
130
+
131
+ private
132
+
133
+ def build(name, file)
134
+ filename = File.basename(file.path)
135
+ mime_type = MIME::Types.type_for(filename)[0] || MIME::Types['application/octet-stream'][0]
136
+
137
+ part = %|--#{BOUNDARY}\r\n|
138
+ part << %|Content-Disposition: form-data; name="#{CGI.escape(name)}"; filename="#{filename}"\r\n|
139
+ part << %|Content-Length: #{file.size}\r\n|
140
+ part << %|Content-Type: #{mime_type.simplified}\r\n|
141
+ part << %|Content-Transfer-Encoding: binary\r\n|
142
+ part << %|\r\n|
143
+ part
144
+ end
145
+ end
146
+
147
+ #
148
+ # The end of the entire request
149
+ #
150
+ class EndingPart
151
+ def initialize
152
+ @part = "--#{BOUNDARY}--\r\n\r\n"
153
+ end
154
+
155
+ def io
156
+ @io ||= StringIO.new(@part)
157
+ end
158
+
159
+ def size
160
+ @part.bytesize
161
+ end
162
+ end
163
+ end
164
+ end