lib/onering/api.rb in onering-client-0.0.46 vs lib/onering/api.rb in onering-client-0.0.50

- old
+ new

@@ -1,221 +1,276 @@ -require 'rest_client' -require 'uri' -require 'multi_json' require 'yaml' -require 'addressable/uri' +require 'hashlib' require 'deep_merge' +require 'addressable/uri' +require 'active_support/core_ext' +require 'httparty' module Onering - module API + class API + module Actions + class Retry < ::Exception; end + end + module Errors + class Exception < ::Exception; end class NotConnected < Exception; end class ClientError < Exception; end class ServerError < Exception; end class ConnectionTimeout < Exception; end - class ClientPemNotFound < Exception; end + class AuthenticationMissing < Exception; end end - class Base - include Onering::Util + include Onering::Util + include ::HTTParty - DEFAULT_BASE="https://onering" - DEFAULT_PATH="/api" - DEFAULT_OPTIONS_FILE=[ - "~/.onering/cli.yml", - "/etc/onering/cli.yml" - ] + attr_accessor :url + format :json - DEFAULT_CLIENT_PEM=[ - "~/.onering/client.pem", - "/etc/onering/client.pem" - ] + DEFAULT_CONFIG={} + DEFAULT_BASE="https://onering" + DEFAULT_PATH="/api" + DEFAULT_OPTIONS_FILE=["~/.onering/cli.yml", "/etc/onering/cli.yml"] + DEFAULT_CLIENT_PEM=["~/.onering/client.pem", "/etc/onering/client.pem"] + DEFAULT_CLIENT_KEY=["~/.onering/client.key", "/etc/onering/client.key"] + DEFAULT_VALIDATION_PEM="/etc/onering/validation.pem" - DEFAULT_VALIDATION_PEM=[ - "/etc/onering/validation.pem" - ] - class<<self - def connect(options={}) - # list all existing config files from least specific to most - @_configfiles = ([options[:config]] + DEFAULT_OPTIONS_FILE).compact.select{|i| - (File.exists?(File.expand_path(i)) rescue false) - }.reverse + def initialize(options={}) + @_config = {} + @_plugins = {} + @_connection_options = options - # merge all config files with more-specific values overriding less-specific ones - @_config = {} - @_configfiles.each do |i| - c = YAML.load(File.read(File.expand_path(i))) rescue {} - @_config.deep_merge!(c) - end + # load and merge all config file sources + _load_config(@_connection_options[:configfile], @_connection_options.get(:config, {})) - if options[:host].is_a?(URI) - @_uri = options[:host] - elsif options[:host].is_a?(String) - @_uri = Addressable::URI.parse("#{options[:host]}/#{DEFAULT_PATH}") - else - @_uri = Addressable::URI.parse("#{@_config['url'] || DEFAULT_BASE}/#{@_config['apiroot'] || DEFAULT_PATH}") - end + # set API connectivity details + Onering::API.base_uri @_config.get(:url, DEFAULT_BASE) - unless @_uri.nil? - begin - @_pemfile = ([options[:pemfile], @_config['pemfile']]+DEFAULT_CLIENT_PEM).compact.select{|i| - (File.exists?((File.expand_path(i) rescue i)) rescue nil) - }.compact.first + Onering::Reporter.setup() + connect(options) if options.get(:autoconnect, true) + end - raise Errors::ClientPemNotFound if @_pemfile.nil? + def connect(options={}) + # setup authentication + _setup_auth() - @_pem = File.read((File.expand_path(@_pemfile) rescue @_pemfile)) + return self + end - @rest = RestClient::Resource.new("#{@_uri.scheme}://#{@_uri.host}:#{@_uri.port || 443}", { - :timeout => 120, - :open_timeout => 30, - :ssl_client_cert => OpenSSL::X509::Certificate.new(@_pem), - :ssl_client_key => OpenSSL::PKey::RSA.new(@_pem), - :verify_peer => OpenSSL::SSL::VERIFY_PEER - }) - rescue Errors::ClientPemNotFound - # client PEM not present, attempt autoregistration - STDERR.puts("Onering client.pem not found, attempting automatic registration...") + def request(method, endpoint, options={}) + endpoint = [@_config.get(:path, DEFAULT_PATH).strip, endpoint.sub(/^\//,'')].join('/') - begin - @_validation = ([options[:validationfile], @_config['validationfile']]+DEFAULT_VALIDATION_PEM).compact.select{|i| - (File.exists?((File.expand_path(i) rescue i)) rescue nil) - }.compact.first + case method + when :post + rv = Onering::API.post(endpoint, options) + when :put + rv = Onering::API.put(endpoint, options) + when :delete + rv = Onering::API.delete(endpoint, options) + when :head + rv = Onering::API.head(endpoint, options) + else + rv = Onering::API.get(endpoint, options) + end - if @_validation.nil? - raise Errors::ClientError.new("Cannot automatically register client, cannot find validation.pem") - end + if rv.code >= 500 + raise Errors::ServerError.new("HTTP #{rv.code} - #{Onering::Util.http_status(rv.code)} #{rv.parsed_response.get('error.message','') rescue ''}") + elsif rv.code >= 400 + raise Errors::ClientError.new("HTTP #{rv.code} - #{Onering::Util.http_status(rv.code)} #{rv.parsed_response.get('error.message', '') rescue ''}") + else + rv + end + end - @_validation = File.read(@_validation) - @rest = RestClient::Resource.new("#{@_uri.scheme}://#{@_uri.host}:#{@_uri.port || 443}", { - :timeout => 120, - :open_timeout => 30, - :ssl_client_cert => OpenSSL::X509::Certificate.new(@_validation), - :ssl_client_key => OpenSSL::PKey::RSA.new(@_validation), - :verify_peer => OpenSSL::SSL::VERIFY_PEER - }) + def get(endpoint, options={}) + request(:get, endpoint, options) + end + def post(endpoint, options={}, &block) + if block_given? + request(:post, endpoint, options.merge({ + :body => yield + })) + else + request(:post, endpoint, options) + end + end - clients = [{ - :path => "/etc/onering", - :name => (@_config['id'] || File.read("/etc/hardware.id")).strip.chomp, - :keyname => 'system', - :autodelete => true - },{ - :path => "~/.onering", - :name => ENV['USER'], - :keyname => 'cli', - :autodelete => false - }] + def put(endpoint, options={}) + if block_given? + request(:put, endpoint, options.merge({ + :body => yield + })) + else + request(:put, endpoint, options) + end + end - # attempt to autoregister clients from least specific to most (machine account then user account) - clients.each do |client| - # determine if we can create this client - client[:path] = (File.expand_path(client[:path]) rescue client[:path]) - next unless File.writable?(File.dirname(client[:path])) - Dir.mkdir(client[:path]) unless File.directory?(client[:path]) - next unless File.writable?(client[:path]) + def delete(endpoint, options={}) + request(:delete, endpoint, options) + end - begin - response = @rest["/api/users/#{client[:name]}/keys/#{client[:keyname]}"].get({ - :params => { - :cert => 'pem', - :autodelete => client[:autodelete] - } - }) - rescue RestClient::Forbidden - STDERR.puts("Cannot re-download key '#{client[:keyname]}' for client #{client[:name]}. Please remove the client key from Onering and try again.") - next + # I'm not a huge fan of what's happening here, but metaprogramming is hard... + # + # "Don't let the perfect be the enemy of the good." + # + def method_missing(method, *args, &block) + modname = method.to_s.split('_').map(&:capitalize).join - rescue RestClient::Exception => e - raise Errors::ClientError.new("HTTP #{e.http_code}: #{e.message}") - end + if not (plugin = (Onering::API.const_get(modname) rescue nil)).nil? + @_plugins[method] ||= plugin.new.connect(@_connection_options) + return @_plugins[method] + else + super + end + end + def opt(name, default=nil) + @_config.get(name, default) + end - File.open("#{client[:path]}/client.pem", "w") do |file| - file.puts(response.to_str) - STDERR.puts("Successfully registered client key #{client[:name]}:#{client[:keyname]}, key is at #{file.path}") - break - end - end - if clients.select{|i| p = "#{i[:path]}/client.pem"; File.exists?((File.expand_path(p) rescue p)) }.empty? - raise Errors::ClientError.new("Unable to register a Onering client.") - end + def status() + Onering::API.get("/").parsed_response + end - retry - rescue Exception => e - STDERR.puts("Error occurred during autoregistration: #{e.class.name} - #{e.message}") - end - end - else - raise Errors::ClientError.new("Could not parse API URL.") - end - end + private +# ----------------------------------------------------------------------------- + def _load_config(configfile, config={}) + if configfile.nil? + configfile = [] + else + # recursively grab all .yml files if directory is specified + configfile = (File.directory?(configfile) ? Dir.glob(File.join(configfile, "**", "*.yml")).sort : [configfile]) + end - def request(endpoint, options={}) - options = @_config.merge(options) - options[:method] = (options[:method].to_s.downcase.to_sym rescue nil) - request = nil + # list all existing config files from least specific to most + @_configfiles = (configfile + DEFAULT_OPTIONS_FILE).compact.select{|i| + (File.exists?(File.expand_path(i)) rescue false) + }.reverse - uri = Addressable::URI.parse("#{@_uri.to_s}/#{endpoint}") - uri.query_values = options[:fields] if options[:fields] + # merge all config files with more-specific values overriding less-specific ones + @_config = DEFAULT_CONFIG + @_configfiles.each do |i| + c = YAML.load(File.read(File.expand_path(i))) rescue {} + @_config.deep_merge!(c) + end - raise Errors::NotConnected unless @rest + # settings specified in the library override everything + @_config.deep_merge!(config.compact) unless config.empty? + end - begin - case options[:method] - when :post - data = (options[:data].nil? ? nil : MultiJson.dump(options[:data])) - response = @rest[uri.request_uri].post(data, { - :content_type => 'application/json' - }) +# ----------------------------------------------------------------------------- + def _setup_auth() + type = @_config.get('authentication.type', :auto) - when :delete - response = @rest[uri.request_uri].delete() + case type.to_sym + when :token + _setup_auth_token() - when :head - response = @rest[uri.request_uri].head() - else - response = @rest[uri.request_uri].get() - end + else + _setup_auth_ssl() + end + end - rescue RestClient::Unauthorized => e - raise Errors::ClientError.new("You are not authorized to perform this request") - rescue RestClient::Exception => e - raise Errors::ClientError.new("(HTTP #{e.http_code}) #{e.class.name}: #{e.message}") - end +# ----------------------------------------------------------------------------- + def _setup_auth_ssl() + begin + # get first keyfile found + key = (([@_config.get('authentication.keyfile')] + DEFAULT_CLIENT_PEM).compact.select{|i| + (File.exists?(File.expand_path(i)) rescue false) + }).first - begin - rv = (response.empty? ? nil : MultiJson.load(response)) - rescue Exception - rv = response - end + # SSL client key not found, attempt autoregistration... + if key.nil? + if @_config.get('authentication.autoregister', true) + validation_key = @_config.get('authentication.validation_keyfile', DEFAULT_VALIDATION_PEM) + validation_key = (File.expand_path(validation_key) rescue validation_key) - rv - end + # if validation key exists, autoregister + if File.size?(validation_key) + # set the authentication PEM to validation.pem + Onering::API.pem(File.read(validation_key)) - def make_filter(filter) - filter = filter.collect{|k,v| "#{k}/#{v}" } if filter.is_a?(Hash) - filter = filter.collect{|i| i.sub(':','/') }.join("/") if filter.is_a?(Array) - filter - end + # attempt to create client.pem from least-specific to most, first writable path wins + clients = [{ + :path => "/etc/onering", + :name => fact('hardwareid'), + :keyname => 'system', + :autodelete => true + },{ + :path => "~/.onering", + :name => ENV['USER'], + :keyname => 'cli', + :autodelete => false + }] - def echo(obj) - if obj.is_a?(Array) - obj.each do |i| - puts i + # for each client attempt... + clients.each do |client| + # expand and assemble path + client[:path] = (File.expand_path(client[:path]) rescue client[:path]) + keyfile = File.join(client[:path], 'client.pem') + + # skip this if we can't write to the parent directory + next unless File.writable?(client[:path]) + Dir.mkdir(client[:path]) unless File.directory?(client[:path]) + next if File.exists?(keyfile) + + # attempt to create/download the keyfile + response = self.class.get("/api/users/#{client[:name].strip}/keys/#{client[:keyname]}") + + # if successful, write the file + if response.code < 400 and response.body + File.open(keyfile, 'w').puts(response.body) + raise Actions::Retry.new + else + # all errors are fatal at this stage + raise Errors::ClientError.new("Cannot autoregister client: HTTP #{response.code} - #{(response.parsed_response || {}).get('error.message', 'Unknown error')}") + end + end + + # it is an error to not have created a client.pem by now + raise Errors::AuthenticationMissing.new("Cannot autoregister client: keyfile not created") + + else + # cannot autoregister without a validation.pem + raise Errors::AuthenticationMissing.new("Cannot autoregister client: validation keyfile is missing") end + else + raise Errors::AuthenticationMissing.new("Cannot find SSL key and autoregistration is disabled") end + else + Onering::API.pem(File.read((File.expand_path(key) rescue key))) end + + rescue Actions::Retry + retry end end + + +# ----------------------------------------------------------------------------- + def _setup_auth_token() + # get first keyfile found + key = @_config.get('authentication.key') + raise Errors::AuthenticationMissing.new("Cannot find an API token") if key.nil? + + # set auth mechanism + Onering::API.headers({ + 'X-Auth-Mechanism' => 'token' + }) + + # set default parameters + Onering::API.default_params({ + :token => key + }) + end end -end +end \ No newline at end of file