Chef's custom REST client with built-in JSON support and RSA signed header authentication.
Create a REST client object. The supplied url is used as the base for all subsequent requests. For example, when initialized with a base url localhost:4000, a call to get_rest with 'nodes' will make an HTTP GET request to localhost:4000/nodes
# File lib/chef/rest.rb, line 60 def initialize(url, client_name=Chef::Config[:node_name], signing_key_filename=Chef::Config[:client_key], options={}) @url = url @cookies = CookieJar.instance @default_headers = options[:headers] || {} @auth_credentials = AuthCredentials.new(client_name, signing_key_filename) @sign_on_redirect, @sign_request = true, true @redirects_followed = 0 @redirect_limit = 10 @disable_gzip = false handle_options(options) end
Runs an HTTP request to a JSON API with JSON body. File Download not supported.
# File lib/chef/rest.rb, line 244 def api_request(method, url, headers={}, data=false) json_body = data ? Chef::JSONCompat.to_json(data) : nil # Force encoding to binary to fix SSL related EOFErrors # cf. http://tickets.opscode.com/browse/CHEF-2363 # http://redmine.ruby-lang.org/issues/5233 json_body.force_encoding(Encoding::BINARY) if json_body.respond_to?(:force_encoding) raw_http_request(method, url, headers, json_body) end
# File lib/chef/rest.rb, line 400 def authentication_headers(method, url, json_body=nil) request_params = {:http_method => method, :path => url.path, :body => json_body, :host => "#{url.host}:#{url.port}"} request_params[:body] ||= "" auth_credentials.signature_headers(request_params) end
# File lib/chef/rest.rb, line 76 def client_name @auth_credentials.client_name end
# File lib/chef/rest.rb, line 168 def create_url(path) if path =~ /^(http|https):\/\// URI.parse(path) else URI.parse("#{@url}/#{path}") end end
# File lib/chef/rest.rb, line 300 def decompress_body(response) if gzip_disabled? response.body else case response[CONTENT_ENCODING] when GZIP Chef::Log.debug "decompressing gzip response" Zlib::Inflate.new(Zlib::MAX_WBITS + 16).inflate(response.body) when DEFLATE Chef::Log.debug "decompressing deflate response" Zlib::Inflate.inflate(response.body) else response.body end end end
Send an HTTP DELETE request to the path
# File lib/chef/rest.rb, line 145 def delete_rest(path, headers={}) api_request(:DELETE, create_url(path), headers) end
Streams a download to a tempfile, then yields the tempfile to a block. After the download, the tempfile will be closed and unlinked. If you rename the tempfile, it will not be deleted. Beware that if the server streams infinite content, this method will stream it until you run out of disk space.
# File lib/chef/rest.rb, line 164 def fetch(path, headers={}) streaming_request(create_url(path), headers) {|tmp_file| yield tmp_file } end
# File lib/chef/rest.rb, line 418 def follow_redirect raise Chef::Exceptions::RedirectLimitExceeded if @redirects_followed >= redirect_limit @redirects_followed += 1 Chef::Log.debug("Following redirect #{@redirects_followed}/#{redirect_limit}") if @sign_on_redirect yield else @sign_request = false yield end ensure @redirects_followed = 0 @sign_request = true end
Send an HTTP GET request to the path
Using this method to fetch a file is considered deprecated.
path |
The path to GET |
raw |
Whether you want the raw body returned, or JSON inflated. Defaults |
to JSON inflated.
# File lib/chef/rest.rb, line 136 def get_rest(path, raw=false, headers={}) if raw streaming_request(create_url(path), headers) else api_request(:GET, create_url(path), headers) end end
# File lib/chef/rest.rb, line 410 def http_retry_count config[:http_retry_count] end
# File lib/chef/rest.rb, line 406 def http_retry_delay config[:http_retry_delay] end
Send an HTTP POST request to the path
# File lib/chef/rest.rb, line 150 def post_rest(path, json, headers={}) api_request(:POST, create_url(path), headers, json) end
Send an HTTP PUT request to the path
# File lib/chef/rest.rb, line 155 def put_rest(path, json, headers={}) api_request(:PUT, create_url(path), headers, json) end
Runs an HTTP request to a JSON API with raw body. File Download not supported.
# File lib/chef/rest.rb, line 254 def raw_http_request(method, url, headers, body) headers = build_headers(method, url, headers, body) retriable_rest_request(method, url, body, headers) do |rest_request| begin response = rest_request.call {|r| r.read_body} Chef::Log.debug("---- HTTP Status and Header Data: ----") Chef::Log.debug("HTTP #{response.http_version} #{response.code} #{response.msg}") response.each do |header, value| Chef::Log.debug("#{header}: #{value}") end Chef::Log.debug("---- End HTTP Status/Header Data ----") response_body = decompress_body(response) if response.kind_of?(Net::HTTPSuccess) if response['content-type'] =~ /json/ Chef::JSONCompat.from_json(response_body.chomp) else Chef::Log.warn("Expected JSON response, but got content-type '#{response['content-type']}'") response_body end elsif redirect_location = redirected_to(response) follow_redirect {api_request(:GET, create_url(redirect_location))} else # have to decompress the body before making an exception for it. But the body could be nil. response.body.replace(decompress_body(response)) if response.body.respond_to?(:replace) if response['content-type'] =~ /json/ exception = Chef::JSONCompat.from_json(response_body) msg = "HTTP Request Returned #{response.code} #{response.message}: " msg << (exception["error"].respond_to?(:join) ? exception["error"].join(", ") : exception["error"].to_s) Chef::Log.info(msg) end response.error! end rescue Exception => e if e.respond_to?(:chef_rest_request=) e.chef_rest_request = rest_request end raise end end end
Register the client
# File lib/chef/rest.rb, line 89 def register(name=Chef::Config[:node_name], destination=Chef::Config[:client_key]) if (File.exists?(destination) && !File.writable?(destination)) raise Chef::Exceptions::CannotWritePrivateKey, "I cannot write your private key to #{destination} - check permissions?" end nc = Chef::ApiClient.new nc.name(name) catch(:done) do retries = config[:client_registration_retries] || 5 0.upto(retries) do |n| begin response = nc.save(true, true) Chef::Log.debug("Registration response: #{response.inspect}") private_key = if response.respond_to?(:[]) response["private_key"] else response.private_key end unless private_key raise Chef::Exceptions::CannotWritePrivateKey, "The response from the server did not include a private key!" end # Write out the private key ::File.open(destination, "w") {|f| f.chmod(0600) f.print(private_key) } throw :done rescue IOError raise Chef::Exceptions::CannotWritePrivateKey, "I cannot write your private key to #{destination}" rescue Net::HTTPFatalError => e Chef::Log.warn("Failed attempt #{n} of #{retries+1} on client creation") raise unless e.response.code == "500" end end end true end
# File lib/chef/rest.rb, line 360 def retriable_rest_request(method, url, req_body, headers) rest_request = Chef::REST::RESTRequest.new(method, url, req_body, headers) Chef::Log.debug("Sending HTTP Request via #{method} to #{url.host}:#{url.port}#{rest_request.path}") http_attempts = 0 begin http_attempts += 1 yield rest_request rescue SocketError, Errno::ETIMEDOUT => e e.message.replace "Error connecting to #{url} - #{e.message}" raise e rescue Errno::ECONNREFUSED if http_retry_count - http_attempts + 1 > 0 Chef::Log.error("Connection refused connecting to #{url.host}:#{url.port} for #{rest_request.path}, retry #{http_attempts}/#{http_retry_count}") sleep(http_retry_delay) retry end raise Errno::ECONNREFUSED, "Connection refused connecting to #{url.host}:#{url.port} for #{rest_request.path}, giving up" rescue Timeout::Error if http_retry_count - http_attempts + 1 > 0 Chef::Log.error("Timeout connecting to #{url.host}:#{url.port} for #{rest_request.path}, retry #{http_attempts}/#{http_retry_count}") sleep(http_retry_delay) retry end raise Timeout::Error, "Timeout connecting to #{url.host}:#{url.port} for #{rest_request.path}, giving up" rescue Net::HTTPFatalError => e if http_retry_count - http_attempts + 1 > 0 sleep_time = 1 + (2 ** http_attempts) + rand(2 ** http_attempts) Chef::Log.error("Server returned error for #{url}, retrying #{http_attempts}/#{http_retry_count} in #{sleep_time}s") sleep(sleep_time) retry end raise end end
Use api_request instead
# File lib/chef/rest.rb, line 194 def run_request(method, url, headers={}, data=false, limit=nil, raw=false) json_body = data ? Chef::JSONCompat.to_json(data) : nil # Force encoding to binary to fix SSL related EOFErrors # cf. http://tickets.opscode.com/browse/CHEF-2363 # http://redmine.ruby-lang.org/issues/5233 json_body.force_encoding(Encoding::BINARY) if json_body.respond_to?(:force_encoding) headers = build_headers(method, url, headers, json_body, raw) tf, response_body = nil, nil retriable_rest_request(method, url, json_body, headers) do |rest_request| res = rest_request.call do |response| if raw tf = stream_to_tempfile(url, response) else response_body = decompress_body(response) end end case res when Net::HTTPSuccess if res['content-type'] =~ /json/ Chef::JSONCompat.from_json(response_body) else if method == :HEAD true elsif raw tf else response_body end end when Net::HTTPNotModified # Must be tested before Net::HTTPRedirection because it's subclass. false when Net::HTTPRedirection follow_redirect {run_request(method, create_url(res['location']), headers, false, nil, raw)} else if res['content-type'] =~ /json/ exception = Chef::JSONCompat.from_json(response_body) msg = "HTTP Request Returned #{res.code} #{res.message}: " msg << (exception["error"].respond_to?(:join) ? exception["error"].join(", ") : exception["error"].to_s) Chef::Log.warn(msg) end res.error! end end end
# File lib/chef/rest.rb, line 176 def sign_requests? auth_credentials.sign_requests? && @sign_request end
# File lib/chef/rest.rb, line 80 def signing_key @auth_credentials.raw_key end
# File lib/chef/rest.rb, line 72 def signing_key_filename @auth_credentials.key_file end
Makes a streaming download request. Doesn't speak JSON. Streams the response body to a tempfile. If a block is given, it's passed to Tempfile.open(), which means that the tempfile will automatically be unlinked after the block is executed.
If no block is given, the tempfile is returned, which means it's up to you to unlink the tempfile when you're done with it.
# File lib/chef/rest.rb, line 324 def streaming_request(url, headers, &block) headers = build_headers(:GET, url, headers, nil, true) retriable_rest_request(:GET, url, nil, headers) do |rest_request| begin tempfile = nil response = rest_request.call do |r| if block_given? && r.kind_of?(Net::HTTPSuccess) begin tempfile = stream_to_tempfile(url, r, &block) yield tempfile ensure tempfile.close! end else tempfile = stream_to_tempfile(url, r) end end if response.kind_of?(Net::HTTPSuccess) tempfile elsif redirect_location = redirected_to(response) # TODO: test tempfile unlinked when following redirects. tempfile && tempfile.close! follow_redirect {streaming_request(create_url(redirect_location), {}, &block)} else tempfile && tempfile.close! response.error! end rescue Exception => e if e.respond_to?(:chef_rest_request=) e.chef_rest_request = rest_request end raise end end end
Generated with the Darkfish Rdoc Generator 2.