chef-infra-api 0.9.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +201 -0
- data/lib/chef-api.rb +96 -0
- data/lib/chef-api/aclable.rb +35 -0
- data/lib/chef-api/authentication.rb +300 -0
- data/lib/chef-api/boolean.rb +6 -0
- data/lib/chef-api/configurable.rb +80 -0
- data/lib/chef-api/connection.rb +507 -0
- data/lib/chef-api/defaults.rb +197 -0
- data/lib/chef-api/error_collection.rb +44 -0
- data/lib/chef-api/errors.rb +64 -0
- data/lib/chef-api/multipart.rb +164 -0
- data/lib/chef-api/resource.rb +21 -0
- data/lib/chef-api/resources/base.rb +960 -0
- data/lib/chef-api/resources/client.rb +84 -0
- data/lib/chef-api/resources/collection_proxy.rb +234 -0
- data/lib/chef-api/resources/cookbook.rb +24 -0
- data/lib/chef-api/resources/cookbook_version.rb +23 -0
- data/lib/chef-api/resources/data_bag.rb +136 -0
- data/lib/chef-api/resources/data_bag_item.rb +53 -0
- data/lib/chef-api/resources/environment.rb +16 -0
- data/lib/chef-api/resources/group.rb +16 -0
- data/lib/chef-api/resources/node.rb +20 -0
- data/lib/chef-api/resources/organization.rb +22 -0
- data/lib/chef-api/resources/partial_search.rb +44 -0
- data/lib/chef-api/resources/principal.rb +11 -0
- data/lib/chef-api/resources/role.rb +18 -0
- data/lib/chef-api/resources/search.rb +47 -0
- data/lib/chef-api/resources/user.rb +82 -0
- data/lib/chef-api/schema.rb +150 -0
- data/lib/chef-api/util.rb +119 -0
- data/lib/chef-api/validator.rb +16 -0
- data/lib/chef-api/validators/base.rb +82 -0
- data/lib/chef-api/validators/required.rb +11 -0
- data/lib/chef-api/validators/type.rb +23 -0
- data/lib/chef-api/version.rb +3 -0
- data/templates/errors/abstract_method.erb +5 -0
- data/templates/errors/cannot_regenerate_key.erb +1 -0
- data/templates/errors/chef_api_error.erb +1 -0
- data/templates/errors/file_not_found.erb +1 -0
- data/templates/errors/http_bad_request.erb +3 -0
- data/templates/errors/http_forbidden_request.erb +3 -0
- data/templates/errors/http_gateway_timeout.erb +3 -0
- data/templates/errors/http_method_not_allowed.erb +3 -0
- data/templates/errors/http_not_acceptable.erb +3 -0
- data/templates/errors/http_not_found.erb +3 -0
- data/templates/errors/http_server_unavailable.erb +1 -0
- data/templates/errors/http_unauthorized_request.erb +3 -0
- data/templates/errors/insufficient_file_permissions.erb +1 -0
- data/templates/errors/invalid_resource.erb +1 -0
- data/templates/errors/invalid_validator.erb +1 -0
- data/templates/errors/missing_url_parameter.erb +1 -0
- data/templates/errors/not_a_directory.erb +1 -0
- data/templates/errors/resource_already_exists.erb +1 -0
- data/templates/errors/resource_not_found.erb +1 -0
- data/templates/errors/resource_not_mutable.erb +1 -0
- data/templates/errors/unknown_attribute.erb +1 -0
- 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
|