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