Merge branch 'master' into 5538-arvadosclient-retry
[arvados.git] / sdk / cli / bin / arv-tag
1 #! /usr/bin/env ruby
2
3 # arv tag usage:
4 #   arv tag add tag1 [tag2 ...] --object obj_uuid1 [--object obj_uuid2 ...]
5 #   arv tag remove tag1 [tag2 ...] --object obj_uuid1 [--object obj_uuid2 ...]
6 #   arv tag remove tag1 [tag2 ...] --all
7
8 def usage_string
9   return "\nUsage:\n" +
10     "arv tag add tag1 [tag2 ...] --object object_uuid1 [object_uuid2...]\n" +
11     "arv tag remove tag1 [tag2 ...] --object object_uuid1 [object_uuid2...]\n" +
12     "arv tag remove --all\n"
13 end
14
15 def usage
16   abort usage_string
17 end
18
19 def api_call(method, parameters:{}, request_body:{})
20   result = $client.execute(:api_method => method,
21                            :parameters => parameters,
22                            :body_object => request_body,
23                            :authenticated => false,
24                            :headers => {
25                              authorization: "OAuth2 #{ENV['ARVADOS_API_TOKEN']}",
26                            })
27
28   begin
29     results = JSON.parse result.body
30   rescue JSON::ParserError => e
31     abort "Failed to parse server response:\n" + e.to_s
32   end
33
34   if results["errors"]
35     abort "Error: #{results["errors"][0]}"
36   end
37
38   return results
39 end
40
41 def tag_add(tag, obj_uuid)
42   return api_call($arvados.links.create,
43                   request_body: {
44                     :link => {
45                       :name       => tag,
46                       :link_class => :tag,
47                       :head_uuid  => obj_uuid,
48                     }
49                   })
50 end
51
52 def tag_remove(tag, obj_uuids=nil)
53   # If we got a list of objects to untag, look up the uuids for the
54   # links that need to be deleted.
55   link_uuids = []
56   if obj_uuids
57     obj_uuids.each do |uuid|
58       link = api_call($arvados.links.list,
59                       request_body: {
60                         :where => {
61                           :link_class => :tag,
62                           :name => tag,
63                           :head_uuid => uuid,
64                         }
65                       })
66       if link['items_available'] > 0
67         link_uuids.push link['items'][0]['uuid']
68       end
69     end
70   else
71     all_tag_links = api_call($arvados.links.list,
72                              request_body: {
73                                :where => {
74                                  :link_class => :tag,
75                                  :name => tag,
76                                }
77                              })
78     link_uuids = all_tag_links['items'].map { |obj| obj['uuid'] }
79   end
80
81   results = []
82   if link_uuids
83     link_uuids.each do |uuid|
84       results.push api_call($arvados.links.delete, parameters:{ :uuid => uuid })
85     end
86   else
87     $stderr.puts "no tags found to remove"
88   end
89
90   return results
91 end
92
93 if RUBY_VERSION < '1.9.3' then
94   abort <<-EOS
95 #{$0.gsub(/^\.\//,'')} requires Ruby version 1.9.3 or higher.
96 EOS
97 end
98
99 $arvados_api_version = ENV['ARVADOS_API_VERSION'] || 'v1'
100 $arvados_api_host = ENV['ARVADOS_API_HOST'] or
101   abort "#{$0}: fatal: ARVADOS_API_HOST environment variable not set."
102 $arvados_api_token = ENV['ARVADOS_API_TOKEN'] or
103   abort "#{$0}: fatal: ARVADOS_API_TOKEN environment variable not set."
104 $arvados_api_host_insecure = %w(1 true yes).
105   include?((ENV['ARVADOS_API_HOST_INSECURE'] || "").downcase)
106
107 begin
108   require 'rubygems'
109   require 'google/api_client'
110   require 'json'
111   require 'pp'
112   require 'oj'
113   require 'trollop'
114 rescue LoadError
115   abort <<-EOS
116 #{$0}: fatal: some runtime dependencies are missing.
117 Try: gem install pp google-api-client json trollop
118   EOS
119 end
120
121 def debuglog(message, verbosity=1)
122   $stderr.puts "#{File.split($0).last} #{$$}: #{message}" if $debuglevel >= verbosity
123 end
124
125 module Kernel
126   def suppress_warnings
127     original_verbosity = $VERBOSE
128     $VERBOSE = nil
129     result = yield
130     $VERBOSE = original_verbosity
131     return result
132   end
133 end
134
135 if $arvados_api_host_insecure or $arvados_api_host.match /local/
136   # You probably don't care about SSL certificate checks if you're
137   # testing with a dev server.
138   suppress_warnings { OpenSSL::SSL::VERIFY_PEER = OpenSSL::SSL::VERIFY_NONE }
139 end
140
141 class Google::APIClient
142   def discovery_document(api, version)
143     api = api.to_s
144     return @discovery_documents["#{api}:#{version}"] ||=
145       begin
146         response = self.execute!(
147                                  :http_method => :get,
148                                  :uri => self.discovery_uri(api, version),
149                                  :authenticated => false
150                                  )
151         response.body.class == String ? JSON.parse(response.body) : response.body
152       end
153   end
154 end
155
156 global_opts = Trollop::options do
157   banner usage_string
158   banner ""
159   opt :dry_run, "Don't actually do anything", :short => "-n"
160   opt :verbose, "Print some things on stderr", :short => "-v"
161   opt :uuid, "Return the UUIDs of the objects in the response, one per line (default)", :short => nil
162   opt :json, "Return the entire response received from the API server, as a JSON object", :short => "-j"
163   opt :human, "Return the response received from the API server, as a JSON object with whitespace added for human consumption", :short => "-h"
164   opt :pretty, "Synonym of --human", :short => nil
165   opt :yaml, "Return the response received from the API server, in YAML format", :short => "-y"
166   stop_on ['add', 'remove']
167 end
168
169 p = Trollop::Parser.new do
170   opt(:all,
171       "Remove this tag from all objects under your ownership. Only valid with `tag remove'.",
172       :short => :none)
173   opt(:object,
174       "The UUID of an object to which this tag operation should be applied.",
175       :type => :string,
176       :multi => true,
177       :short => :o)
178 end
179
180 $options = Trollop::with_standard_exception_handling p do
181   p.parse ARGV
182 end
183
184 if $options[:all] and ARGV[0] != 'remove'
185   usage
186 end
187
188 # Set up the API client.
189
190 $client ||= Google::APIClient.
191   new(:host => $arvados_api_host,
192       :application_name => File.split($0).last,
193       :application_version => $application_version.to_s)
194 $arvados = $client.discovered_api('arvados', $arvados_api_version)
195
196 results = []
197 cmd = ARGV.shift
198
199 if ARGV.empty?
200   usage
201 end
202
203 case cmd
204 when 'add'
205   ARGV.each do |tag|
206     $options[:object].each do |obj|
207       results.push(tag_add(tag, obj))
208     end
209   end
210 when 'remove'
211   ARGV.each do |tag|
212     if $options[:all] then
213       results.concat tag_remove(tag)
214     else
215       results.concat tag_remove(tag, $options[:object])
216     end
217   end
218 else
219   usage
220 end
221
222 if global_opts[:human] or global_opts[:pretty] then
223   puts Oj.dump(results, :indent => 1)
224 elsif global_opts[:yaml] then
225   puts results.to_yaml
226 elsif global_opts[:json] then
227   puts Oj.dump(results)
228 else
229   results.each do |r|
230     if r['uuid'].nil?
231       abort("Response did not include a uuid:\n" +
232             Oj.dump(r, :indent => 1) +
233             "\n")
234     else
235       puts r['uuid']
236     end
237   end
238 end