lib/chef-api/connection.rb in chef-api-0.2.0 vs lib/chef-api/connection.rb in chef-api-0.2.1

- old
+ new

@@ -14,37 +14,58 @@ # # @private # # @macro proxy # @method $1 - # Get a proxied collection for +$1+. The proxy automatically injects - # the current connection into the $2, providing a very Rubyesque way - # for handling multiple connection objects. + # Get the list of $1 for this {Connection}. This method is threadsafe. # - # @example Get the $1 from the connection object + # @example Get the $1 from this {Connection} object # connection = ChefAPI::Connection.new('...') - # connection.$1 #=> $2 (with the connection object pre-populated) + # connection.$1 #=> $2(attribute1, attribute2, ...) # - # @return [ChefAPI::Proxy<$2>] - # a collection proxy for the $2 + # @return [Class<$2>] # def proxy(name, klass) class_eval <<-EOH, __FILE__, __LINE__ + 1 def #{name} - @#{name} ||= ChefAPI::Proxy.new(self, #{klass}) + Thread.current['chefapi.connection'] = self + #{klass} end EOH end end + include Logify include ChefAPI::Configurable - include ChefAPI::Logger + proxy :clients, 'Resource::Client' + proxy :cookbooks, 'Resource::Cookbook' + proxy :data_bags, 'Resource::DataBag' + proxy :environments, 'Resource::Environment' + proxy :nodes, 'Resource::Node' + proxy :principals, 'Resource::Principal' + proxy :roles, 'Resource::Role' + proxy :users, 'Resource::User' + # # Create a new ChefAPI Connection with the given options. Any options # given take precedence over the default options. # + # @example Create a connection object from a list of options + # ChefAPI::Connection.new( + # endpoint: 'https://...', + # client: 'bacon', + # key: '~/.chef/bacon.pem', + # ) + # + # @example Create a connection object using a block + # ChefAPI::Connection.new do |connection| + # connection.endpoint = 'https://...' + # connection.client = 'bacon' + # connection.key = '~/.chef/bacon.pem' + # end + # # @return [ChefAPI::Connection] # def initialize(options = {}) # Use any options given, but fall back to the defaults set on the module ChefAPI::Configurable.keys.each do |key| @@ -54,10 +75,12 @@ options[key] end instance_variable_set(:"@#{key}", value) end + + yield self if block_given? end # # Determine if the given options are the same as ours. # @@ -147,18 +170,17 @@ # @param [String] path # the absolute or relative path from {Defaults.endpoint} to make the # request against # @param [#read, Hash, nil] data # the data to use (varies based on the +verb+) - # @param [Hash] headers - # the list of headers to use # # @return [String, Hash] # the response body # def request(verb, path, data = {}) - log.info "===> #{verb.to_s.upcase} #{path}..." + log.info "#{verb.to_s.upcase} #{path}..." + log.debug "Chef flavor: #{flavor.inspect}" # Build the URI and request object from the given information uri = build_uri(verb, path, data) request = class_for_request(verb).new(uri.request_uri) @@ -199,11 +221,11 @@ end # Naughty, naughty, naughty! Don't blame when when someone hops in # and executes a MITM attack! unless ssl_verify - log.warn "===> Disabling SSL verification..." + log.warn "Disabling SSL verification..." log.warn "Neither ChefAPI nor the maintainers are responsible for " \ "damanges incurred as a result of disabling SSL verification. " \ "Please use this with extreme caution, or consider specifying " \ "a custom certificate using `config.ssl_pem_file'." connection.verify_mode = OpenSSL::SSL::VERIFY_NONE @@ -213,23 +235,26 @@ # Create a connection using the block form, which will ensure the socket # is properly closed in the event of an error. connection.start do |http| response = http.request(request) + log.debug "Raw response:" + log.debug response.body + case response when Net::HTTPRedirection - redirect = URI.parse(response['location']) - log.debug "===> Performing HTTP redirect to #{redirect}" - request(verb, redirect, params) + redirect = URI.parse(response['location']).to_s + log.debug "Performing HTTP redirect to #{redirect}" + request(verb, redirect, data) when Net::HTTPSuccess success(response) else error(response) end end rescue SocketError, Errno::ECONNREFUSED, EOFError - log.warn " Unable to reach the Chef Server" + log.warn "Unable to reach the Chef Server" raise Error::HTTPServerUnavailable.new end # # Construct a URL from the given verb and path. If the request is a GET or @@ -248,35 +273,38 @@ # the list of params to build the URI with (for GET and DELETE requests) # # @return [URI] # def build_uri(verb, path, params = {}) - log.info "===> Building URI..." + log.info "Building URI..." # Add any query string parameters if [:delete, :get].include?(verb) - log.debug " Detected verb deserves a querystring" - log.debug " Building querystring using #{params.inspect}" - path = [path, to_query_string(params)].compact.join('?') + if querystring = to_query_string(params) + log.debug "Detected verb deserves a querystring" + log.debug "Building querystring using #{params.inspect}" + log.debug "Compiled querystring is #{querystring.inspect}" + path = [path, querystring].compact.join('?') + end end # Parse the URI uri = URI.parse(path) # Don't merge absolute URLs unless uri.absolute? - log.debug " Detected URI is relative" - log.debug " Appending #{endpoint} to #{path}" + log.debug "Detected URI is relative" + log.debug "Appending #{path} to #{endpoint}" uri = URI.parse(File.join(endpoint, path)) end # Return the URI object uri end # - # Helper method to get the corresponding {Net::HTTP} class from the given + # Helper method to get the corresponding +Net::HTTP+ class from the given # HTTP verb. # # @param [#to_s] verb # the HTTP verb to create a class from # @@ -321,28 +349,28 @@ # the RSA private key as an OpenSSL object # def parsed_key return @parsed_key if @parsed_key - log.info "===> Parsing private key..." + log.info "Parsing private key..." if key.nil? - log.warn " No private key given!" + log.warn "No private key given!" raise 'No private key given!' end if key.is_a?(OpenSSL::PKey::RSA) - log.debug " Detected private key is an OpenSSL Ruby object" + log.debug "Detected private key is an OpenSSL Ruby object" @parsed_key = key end - if key =~ /(.+)\.pem$/ || File.exists?(key) - log.debug " Detected private key is the path to a file" + if key =~ /(.+)\.pem$/ || File.exists?(File.expand_path(key)) + log.debug "Detected private key is the path to a file" contents = File.read(File.expand_path(key)) @parsed_key = OpenSSL::PKey::RSA.new(contents) else - log.debug " Detected private key was the literal string key" + log.debug "Detected private key was the literal string key" @parsed_key = OpenSSL::PKey::RSA.new(key) end @parsed_key end @@ -357,19 +385,19 @@ # # @return [String, Hash] # the parsed response, as an object # def success(response) - log.info "===> Parsing response as success..." + log.info "Parsing response as success..." case response['Content-Type'] - when 'application/json' - log.debug " Detected response as JSON" - log.debug " Parsing response body as JSON" + when /json/ + log.debug "Detected response as JSON" + log.debug "Parsing response body as JSON" JSON.parse(response.body) else - log.debug " Detected response as text/plain" + log.debug "Detected response as text/plain" response.body end end # @@ -378,19 +406,19 @@ # # @param [HTTP::Message] response # the response object from the request # def error(response) - log.info "===> Parsing response as error..." + log.info "Parsing response as error..." case response['Content-Type'] - when 'application/json' - log.debug " Detected error response as JSON" - log.debug " Parsing error response as JSON" - message = JSON.parse(response.body)['error'].first + when /json/ + log.debug "Detected error response as JSON" + log.debug "Parsing error response as JSON" + message = Array(JSON.parse(response.body)['error']).join(', ') else - log.debug " Detected response as text/plain" + log.debug "Detected response as text/plain" message = response.body end case response.code.to_i when 400 @@ -403,10 +431,12 @@ raise Error::HTTPNotFound.new(message: message) when 405 raise Error::HTTPMethodNotAllowed.new(message: message) when 406 raise Error::HTTPNotAcceptable.new(message: message) + when 504 + raise Error::HTTPGatewayTimeout.new(message: message) when 500..600 raise Error::HTTPServerUnavailable.new else raise "I got an error #{response.code} that I don't know how to handle!" end @@ -416,11 +446,11 @@ # Adds the default headers to the request object. # # @param [Net::HTTP::Request] request # def add_request_headers(request) - log.info "===> Adding request headers..." + log.info "Adding request headers..." headers = { 'Accept' => 'application/json', 'Content-Type' => 'application/json', 'Connection' => 'keep-alive', @@ -428,38 +458,38 @@ 'User-Agent' => user_agent, 'X-Chef-Version' => '11.4.0', } headers.each do |key, value| - log.debug " #{key}: #{value}" + log.debug "#{key}: #{value}" request[key] = value end end # # Use mixlib-auth to create a signed header auth. # # @param [Net::HTTP::Request] request # def add_signing_headers(verb, uri, request, key) - log.info "===> Adding signed header authentication..." + log.info "Adding signed header authentication..." - unless defined?(Mixlib::Authentication) + unless defined?(Mixlib::Authentication::SignedHeaderAuth) require 'mixlib/authentication/signedheaderauth' end headers = Mixlib::Authentication::SignedHeaderAuth.signing_object( :http_method => verb, :body => request.body || '', :host => "#{uri.host}:#{uri.port}", - :path => request.path, + :path => uri.path, :timestamp => Time.now.utc.iso8601, :user_id => client, :file => '', ).sign(key) headers.each do |key, value| - log.debug " #{key}: #{value}" + log.debug "#{key}: #{value}" request[key] = value end end end end