X-Git-Url: https://git.arvados.org/arvados.git/blobdiff_plain/34f7549bb4dba0f35290b473fa7b1d3d6654b90c..4ad6191d53207a8b2d4c0c8a30b18119daaa5fbc:/sdk/ruby/lib/arvados.rb diff --git a/sdk/ruby/lib/arvados.rb b/sdk/ruby/lib/arvados.rb index 51a1d403a6..63550cd37c 100644 --- a/sdk/ruby/lib/arvados.rb +++ b/sdk/ruby/lib/arvados.rb @@ -1,23 +1,44 @@ +# Copyright (C) The Arvados Authors. All rights reserved. +# +# SPDX-License-Identifier: Apache-2.0 + require 'rubygems' -require 'google/api_client' require 'active_support/inflector' require 'json' +require 'fileutils' +require 'andand' +require 'net/http' + +require 'arvados/google_api_client' ActiveSupport::Inflector.inflections do |inflect| inflect.irregular 'specimen', 'specimens' + inflect.irregular 'human', 'humans' end -module Kernel - def suppress_warnings - original_verbosity = $VERBOSE - $VERBOSE = nil - result = yield - $VERBOSE = original_verbosity - return result +class Arvados + class ArvadosClient < Google::APIClient + attr_reader :request_id + + def execute(*args) + @request_id = "req-" + Random.new.rand(2**128).to_s(36)[0..19] + if args.last.is_a? Hash + args.last[:headers] ||= {} + args.last[:headers]['X-Request-Id'] = @request_id + end + begin + super(*args) + rescue => e + if !e.message.match(/.*req-[0-9a-zA-Z]{20}.*/) + raise $!, "#{$!} (Request ID: #{@request_id})", $!.backtrace + end + raise e + end + end end -end -class Arvados + class TransactionFailedError < StandardError + end @@debuglevel = 0 class << self @@ -28,19 +49,22 @@ class Arvados @application_version ||= 0.0 @application_name ||= File.split($0).last - @arvados_api_version = opts[:api_version] || - ENV['ARVADOS_API_VERSION'] || - 'v1' - @arvados_api_host = opts[:api_host] || - ENV['ARVADOS_API_HOST'] or - raise "#{$0}: no :api_host or ENV[ARVADOS_API_HOST] provided." - @arvados_api_token = opts[:api_token] || - ENV['ARVADOS_API_TOKEN'] or - raise "#{$0}: no :api_token or ENV[ARVADOS_API_TOKEN] provided." + @arvados_api_version = opts[:api_version] || 'v1' - @suppress_ssl_warnings = opts[:suppress_ssl_warnings] || false + @config = nil + [[:api_host, 'ARVADOS_API_HOST'], + [:api_token, 'ARVADOS_API_TOKEN']].each do |op, en| + if opts[op] + config[en] = opts[op] + end + if !config[en] + raise "#{$0}: no :#{op} or ENV[#{en}] provided." + end + end - if @suppress_ssl_warnings + if (opts[:suppress_ssl_warnings] or + %w(1 true yes).index(config['ARVADOS_API_HOST_INSECURE']. + andand.downcase)) suppress_warnings do OpenSSL::SSL.const_set 'VERIFY_PEER', OpenSSL::SSL::VERIFY_NONE end @@ -50,8 +74,9 @@ class Arvados # resource. After this, self.job will return Arvados::Job; # self.job.new() and self.job.find() will do what you want. _arvados = self + namespace_class = Arvados.const_set "A#{self.object_id}", Class.new self.arvados_api.schemas.each do |classname, schema| - next if classname.match /List$/ + next if classname.match(/List$/) klass = Class.new(Arvados::Model) do def self.arvados @arvados @@ -69,11 +94,10 @@ class Arvados arvados_api. send(classname.underscore.split('/').last.pluralize.to_sym). discovered_methods. - collect(&:name). - each do |method_name| + each do |method| class << klass; self; end.class_eval do - define_method method_name do |*params| - self.api_exec(method_name.to_sym, *params) + define_method method.name do |*params| + self.api_exec method, *params end end end @@ -81,41 +105,25 @@ class Arvados # Give the new class access to the API klass.instance_eval do @arvados = _arvados - # These should be pulled from the discovery document instead: + # TODO: Pull these from the discovery document instead. @api_models_sym = classname.underscore.split('/').last.pluralize.to_sym @api_model_sym = classname.underscore.split('/').last.to_sym end - # This might produce confusing results when using multiple - # Arvados instances. - Arvados.const_set classname, klass + # Create the new class in namespace_class so it doesn't + # interfere with classes created by other Arvados objects. The + # result looks like Arvados::A26949680::Job. + namespace_class.const_set classname, klass - self.class.class_eval do - define_method classname.underscore do - klass - end + self.define_singleton_method classname.underscore do + klass end end end - class Google::APIClient - def discovery_document(api, version) - api = api.to_s - return @discovery_documents["#{api}:#{version}"] ||= - begin - response = self.execute!( - :http_method => :get, - :uri => self.discovery_uri(api, version), - :authenticated => false - ) - response.body.class == String ? JSON.parse(response.body) : response.body - end - end - end - def client - @client ||= Google::APIClient. - new(:host => @arvados_api_host, + @client ||= ArvadosClient. + new(:host => config["ARVADOS_API_HOST"], :application_name => @application_name, :application_version => @application_version.to_s) end @@ -128,6 +136,68 @@ class Arvados $stderr.puts "#{File.split($0).last} #{$$}: #{message}" if @@debuglevel >= verbosity end + def debuglog *args + self.class.debuglog(*args) + end + + def config(config_file_path="~/.config/arvados/settings.conf") + return @config if @config + + # Initialize config settings with environment variables. + config = {} + config['ARVADOS_API_HOST'] = ENV['ARVADOS_API_HOST'] + config['ARVADOS_API_TOKEN'] = ENV['ARVADOS_API_TOKEN'] + config['ARVADOS_API_HOST_INSECURE'] = ENV['ARVADOS_API_HOST_INSECURE'] + + if config['ARVADOS_API_HOST'] and config['ARVADOS_API_TOKEN'] + # Environment variables take precedence over the config file, so + # there is no point reading the config file. If the environment + # specifies a _HOST without asking for _INSECURE, we certainly + # shouldn't give the config file a chance to create a + # system-wide _INSECURE state for this user. + # + # Note: If we start using additional configuration settings from + # this file in the future, we might have to read the file anyway + # instead of returning here. + return (@config = config) + end + + begin + expanded_path = File.expand_path config_file_path + if File.exist? expanded_path + # Load settings from the config file. + lineno = 0 + File.open(expanded_path).each do |line| + lineno = lineno + 1 + # skip comments and blank lines + next if line.match('^\s*#') or not line.match('\S') + var, val = line.chomp.split('=', 2) + var.strip! + val.strip! + # allow environment settings to override config files. + if !var.empty? and val + config[var] ||= val + else + debuglog "#{expanded_path}: #{lineno}: could not parse `#{line}'", 0 + end + end + end + rescue StandardError => e + debuglog "Ignoring error reading #{config_file_path}: #{e}", 0 + end + + @config = config + end + + def cluster_config + return @cluster_config if @cluster_config + + uri = URI("https://#{config()["ARVADOS_API_HOST"]}/arvados/v1/config") + cc = JSON.parse(Net::HTTP.get(uri)) + + @cluster_config = cc + end + class Model def self.arvados_api arvados.arvados_api @@ -136,22 +206,50 @@ class Arvados arvados.client end def self.debuglog(*args) - arvados.class.debuglog *args + arvados.class.debuglog(*args) end def debuglog(*args) - self.class.arvados.class.debuglog *args + self.class.arvados.class.debuglog(*args) end def self.api_exec(method, parameters={}) - parameters = parameters. - merge(:api_token => ENV['ARVADOS_API_TOKEN']) + api_method = arvados_api.send(api_models_sym).send(method.name.to_sym) parameters.each do |k,v| parameters[k] = v.to_json if v.is_a? Array or v.is_a? Hash end + # Look for objects expected by request.properties.(key).$ref and + # move them from parameters (query string) to request body. + body = nil + method.discovery_document['request']. + andand['properties']. + andand.each do |k,v| + if v.is_a? Hash and v['$ref'] + body ||= {} + body[k] = parameters.delete k.to_sym + end + end result = client. - execute(:api_method => arvados_api.send(api_models_sym).send(method), + execute(:api_method => api_method, :authenticated => false, - :parameters => parameters) - JSON.parse result.body, :symbolize_names => true + :parameters => parameters, + :body_object => body, + :headers => { + :authorization => 'Bearer '+arvados.config['ARVADOS_API_TOKEN'] + }) + resp = JSON.parse result.body, :symbolize_names => true + if resp[:errors] + if !resp[:errors][0].match(/.*req-[0-9a-zA-Z]{20}.*/) + resp[:errors][0] += " (#{result.headers['X-Request-Id'] or client.request_id})" + end + raise Arvados::TransactionFailedError.new(resp[:errors]) + elsif resp[:uuid] and resp[:etag] + self.new(resp) + elsif resp[:items].is_a? Array + resp.merge(:items => resp[:items].collect do |i| + self.new(i) + end) + else + resp + end end def []=(x,y) @@ -175,7 +273,7 @@ class Arvados :uuid => @attributes[:uuid], self.class.api_model_sym => @attributes_to_update.to_json } - unless j.is_a? Hash and j[:uuid] + unless j.respond_to? :[] and j[:uuid] debuglog "Failed to save #{self.to_s}: #{j[:errors] rescue nil}", 0 nil else @@ -191,4 +289,16 @@ class Arvados @attributes = j end end + + protected + + def suppress_warnings + original_verbosity = $VERBOSE + begin + $VERBOSE = nil + yield + ensure + $VERBOSE = original_verbosity + end + end end