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