etna 0.1.37 → 0.1.41

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 484c950fffbafcf5132df923bc0ef88a79faa6c285ccde653805aac526e4a97e
4
- data.tar.gz: f513b75b048e816acc494c09cb0eccbc2c38cda02c2723266fe7bd98c8595f3a
3
+ metadata.gz: 44b6c9fde1d01fab95038052180531fafd4a120e77ffbd12a8ac14e28122cd05
4
+ data.tar.gz: e1cb315eaeb4d35efc9e7f181e0cad767833b98f7ce3f26386e040c758c8483c
5
5
  SHA512:
6
- metadata.gz: 6ad0e99925dc6110c8f80f4a1e1417d610b28838497f09afe8be1c69a65fec95a7d549af987aaccde2b42480347bf336838be5398ef580d3024476206b4d611d
7
- data.tar.gz: 44eb05596c635e7ab96df7971fed6b24bfee7bd545238e173e3d1b57bdcfe7b30db5deea355d2711b192d026d8fb72afc32cbcbcf4a83ef92848184433b7a813
6
+ metadata.gz: 3129a01c4181a772ab6752ed319396485e06f774b8091bff1ed9cbf6543a34820e0cc0706fd313ce5b3fb848a25f347de17bc376dc66b36458975d6298f48608
7
+ data.tar.gz: 82c57cff18c826a0a2664a63e0a531df57c75af30ceb3408d1d8ec1b9cb942fd25bd11253e6d880db6973df4cd60ade0764fac798a543af1e7601bfcc21abc45
data/lib/etna.rb CHANGED
@@ -22,6 +22,7 @@ require_relative './etna/filesystem'
22
22
  require_relative './etna/formatting'
23
23
  require_relative './etna/cwl'
24
24
  require_relative './etna/metrics'
25
+ require_relative './etna/remote'
25
26
 
26
27
  class EtnaApp
27
28
  include Etna::Application
@@ -61,11 +61,49 @@ module Etna::Application
61
61
  end
62
62
 
63
63
  def setup_yabeda
64
+ application = self.id
65
+ Yabeda.configure do
66
+ default_tag :application, application
67
+
68
+ group :etna do
69
+ histogram :response_time do
70
+ comment "Time spent by a controller returning a response"
71
+ unit :seconds
72
+ tags [:controller, :action, :user_hash, :project_name]
73
+ buckets [0.01, 0.1, 0.3, 0.5, 1, 5]
74
+ end
75
+
76
+ counter :visits do
77
+ comment "Counts visits to the controller"
78
+ tags [:controller, :action, :user_hash, :project_name]
79
+ end
80
+
81
+ counter :rollbar_errors do
82
+ comment "Counts errors detected by and sent to rollbar"
83
+ end
84
+
85
+ gauge :last_command_completion do
86
+ comment "Unix time of last time command was completed"
87
+ tags [:command, :status, :application]
88
+ end
89
+
90
+ histogram :command_runtime do
91
+ comment "Time spent processing a given command"
92
+ tags [:command, :status, :application]
93
+ unit :seconds
94
+ buckets [0.1, 1, 5, 60, 300, 1500]
95
+ end
96
+ end
97
+ end
98
+
64
99
  Yabeda.configure!
65
100
  end
66
101
 
102
+ # Writes all metrics currently gathered to a text format prometheus file. If /tmp/metrics.prom is bind mounted
103
+ # to the host directed bound to the node_exporter's file exporter directory, these will be exported to
104
+ # prometheus. Combine this enable_job_metrics! for maximum effect.
67
105
  def write_job_metrics(name)
68
- node_metrics_dir = config(:node_metrics_dir) || "/tmp/metrics.prom"
106
+ node_metrics_dir = "/tmp/metrics.prom"
69
107
  ::FileUtils.mkdir_p(node_metrics_dir)
70
108
 
71
109
  tmp_file = ::File.join(node_metrics_dir, "#{name}.prom.$$")
@@ -126,17 +164,33 @@ module Etna::Application
126
164
  end
127
165
 
128
166
  def run_command(config, *args, &block)
167
+ application = self.id
168
+ status = 'success'
169
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
129
170
  cmd, cmd_args, cmd_kwds = find_command(*args)
130
- cmd.setup(config)
131
171
 
132
- if block_given?
133
- return unless yield [cmd, cmd_args]
134
- end
172
+ begin
173
+ cmd.setup(config)
174
+ if block_given?
175
+ return unless yield [cmd, cmd_args]
176
+ end
135
177
 
136
- cmd.execute(*cmd.fill_in_missing_params(cmd_args), **cmd_kwds)
137
- rescue => e
138
- Rollbar.error(e)
139
- raise
178
+ cmd.execute(*cmd.fill_in_missing_params(cmd_args), **cmd_kwds)
179
+ rescue => e
180
+ Rollbar.error(e)
181
+ status = 'failed'
182
+ raise
183
+ ensure
184
+ if defined?(Yabeda) && Yabeda.configured?
185
+ tags = { command: cmd.class.name, status: status, application: application }
186
+ dur = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
187
+
188
+ Yabeda.etna.last_command_completion.set(tags, Time.now.to_i)
189
+ Yabeda.etna.command_runtime.measure(tags, dur)
190
+
191
+ write_job_metrics("run_command.#{cmd.class.name}")
192
+ end
193
+ end
140
194
  end
141
195
  end
142
196
 
@@ -210,6 +210,52 @@ module Etna
210
210
  }
211
211
  end
212
212
 
213
+ def resolve_conflicts_and_verify_rename(project_name:, source_bucket:, dest_bucket:, folder:, file:)
214
+ parent_folder = ::File.dirname(file.file_path)
215
+
216
+ should_rename_original = true
217
+
218
+ # If the destination folder already exists, check to see if
219
+ # the file also exists, otherwise we risk a
220
+ # rename conflict.
221
+ create_folder_request = CreateFolderRequest.new(
222
+ project_name: project_name,
223
+ bucket_name: dest_bucket,
224
+ folder_path: parent_folder
225
+ )
226
+
227
+ if folder_exists?(create_folder_request)
228
+ # If file exists in destination, delete the older file.
229
+ list_dest_folder_request = Etna::Clients::Metis::ListFolderRequest.new(
230
+ bucket_name: dest_bucket,
231
+ project_name: project_name,
232
+ folder_path: parent_folder
233
+ )
234
+
235
+ dest_file = list_folder(list_dest_folder_request).files.all.find { |f| f.file_name == file.file_name }
236
+
237
+ if (dest_file && file.updated_at <= dest_file.updated_at)
238
+ # Delete source file if it's out of date
239
+ delete_file(Etna::Clients::Metis::DeleteFileRequest.new(
240
+ bucket_name: source_bucket,
241
+ project_name: project_name,
242
+ file_path: file.file_path,
243
+ ))
244
+
245
+ should_rename_original = false
246
+ elsif (dest_file && file.updated_at > dest_file.updated_at)
247
+ # Delete dest file if it's out of date
248
+ delete_file(Etna::Clients::Metis::DeleteFileRequest.new(
249
+ bucket_name: dest_bucket,
250
+ project_name: project_name,
251
+ file_path: dest_file.file_path,
252
+ ))
253
+ end
254
+ end
255
+
256
+ should_rename_original
257
+ end
258
+
213
259
  def recursively_rename_folder(project_name:, source_bucket:, dest_bucket:, folder:)
214
260
  folder_contents = list_folder(
215
261
  Etna::Clients::Metis::ListFolderRequest.new(
@@ -228,6 +274,13 @@ module Etna
228
274
  end
229
275
 
230
276
  folder_contents.files.all.each do |file|
277
+ should_rename = resolve_conflicts_and_verify_rename(
278
+ project_name: project_name,
279
+ source_bucket: source_bucket,
280
+ dest_bucket: dest_bucket,
281
+ folder: folder,
282
+ file: file)
283
+
231
284
  rename_file(Etna::Clients::Metis::RenameFileRequest.new(
232
285
  bucket_name: source_bucket,
233
286
  project_name: project_name,
@@ -235,7 +288,7 @@ module Etna
235
288
  new_bucket_name: dest_bucket,
236
289
  new_file_path: file.file_path,
237
290
  create_parent: true)
238
- )
291
+ ) if should_rename
239
292
  end
240
293
 
241
294
  # Now delete the source folder
@@ -1,3 +1,4 @@
1
- require_relative './workflows/metis_download_workflow'
2
- require_relative './workflows/metis_upload_workflow'
3
- require_relative './workflows/sync_metis_data_workflow'
1
+ require_relative "./workflows/metis_download_workflow"
2
+ require_relative "./workflows/metis_upload_workflow"
3
+ require_relative "./workflows/sync_metis_data_workflow"
4
+ require_relative "./workflows/ingest_metis_data_workflow"
@@ -0,0 +1,35 @@
1
+ require "ostruct"
2
+ require "fileutils"
3
+ require "tempfile"
4
+
5
+ module Etna
6
+ module Clients
7
+ class Metis
8
+ class IngestMetisDataWorkflow < Struct.new(:metis_filesystem, :ingest_filesystem, :logger, keyword_init: true)
9
+ # Since we are doing manual triage of files,
10
+ # do not automatically copy directory trees.
11
+ # srcs must be a list of full paths to files.
12
+ def copy_files(srcs, &block)
13
+ srcs.each do |src|
14
+ if !ingest_filesystem.exist?(src)
15
+ logger&.warn("#{src} does not exist on source filesystem. Skipping.")
16
+ next
17
+ end
18
+
19
+ logger&.info("Copying file #{src} (#{Etna::Formatting.as_size(ingest_filesystem.stat(src).size)})")
20
+
21
+ # For ingestion triage, just copy over the exact path + filename.
22
+ copy_file(dest: src, src: src, &block)
23
+ end
24
+ end
25
+
26
+ def copy_file(dest:, src:, &block)
27
+ ingest_filesystem.with_readable(src, "r") do |io|
28
+ metis_filesystem.do_streaming_upload(io, dest, ingest_filesystem.stat(src).size)
29
+ yield src if block_given?
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -30,7 +30,7 @@ module Etna
30
30
  project_name: project_name,
31
31
  bucket_name: bucket_name,
32
32
  folder_path: dir,
33
- ))
33
+ )) unless dir == "."
34
34
 
35
35
  authorize_response = metis_client.authorize_upload(AuthorizeUploadRequest.new(
36
36
  project_name: project_name,
@@ -19,25 +19,30 @@ module Etna
19
19
  @logger.warn(request_msg(line))
20
20
  end
21
21
 
22
+ def handle_error(e)
23
+ case e
24
+ when Etna::Error
25
+ Rollbar.error(e)
26
+ @logger.error(request_msg("Exiting with #{e.status}, #{e.message}"))
27
+ return failure(e.status, error: e.message)
28
+ else
29
+ Rollbar.error(e)
30
+ @logger.error(request_msg('Caught unspecified error'))
31
+ @logger.error(request_msg(e.message))
32
+ e.backtrace.each do |trace|
33
+ @logger.error(request_msg(trace))
34
+ end
35
+ return failure(500, error: 'Server error.')
36
+ end
37
+ end
38
+
22
39
  def response(&block)
23
40
  return instance_eval(&block) if block_given?
24
-
25
41
  return send(@action) if @action
26
42
 
27
-
28
43
  [501, {}, ['This controller is not implemented.']]
29
- rescue Etna::Error => e
30
- Rollbar.error(e)
31
- @logger.error(request_msg("Exiting with #{e.status}, #{e.message}"))
32
- return failure(e.status, error: e.message)
33
44
  rescue Exception => e
34
- Rollbar.error(e)
35
- @logger.error(request_msg('Caught unspecified error'))
36
- @logger.error(request_msg(e.message))
37
- e.backtrace.each do |trace|
38
- @logger.error(request_msg(trace))
39
- end
40
- return failure(500, error: 'Server error.')
45
+ handle_error(e)
41
46
  end
42
47
 
43
48
  def require_params(*params)
@@ -3,6 +3,7 @@ require 'fileutils'
3
3
  require 'open3'
4
4
  require 'securerandom'
5
5
  require 'concurrent-ruby'
6
+ require 'curb'
6
7
 
7
8
  module Etna
8
9
  # A class that encapsulates opening / reading file system entries that abstracts normal file access in order
@@ -50,6 +51,11 @@ module Etna
50
51
  ::FileUtils.mv(src, dest)
51
52
  end
52
53
 
54
+ def stat(src)
55
+ raise "stat not supported by #{self.class.name}" unless self.class == Filesystem
56
+ ::File.stat(src)
57
+ end
58
+
53
59
  class EmptyIO < StringIO
54
60
  def write(*args)
55
61
  # Do nothing -- always leave empty
@@ -369,7 +375,120 @@ module Etna
369
375
  end
370
376
  end
371
377
 
378
+ class SftpFilesystem < Filesystem
379
+ include WithPipeConsumer
380
+
381
+ class SftpFile
382
+ attr_reader :size, :name
383
+
384
+ def initialize(metadata)
385
+ @metadata_parts = metadata.split(" ")
386
+ @size = @metadata_parts[4].to_i
387
+ @perms = @metadata_parts.first
388
+ @name = @metadata_parts[8]
389
+ end
390
+ end
391
+
392
+ def initialize(host:, username:, password: nil, port: 22, **args)
393
+ @username = username
394
+ @password = password
395
+ @host = host
396
+ @port = port
397
+
398
+ @dir_listings = {}
399
+ end
400
+
401
+ def url(src)
402
+ "sftp://#{@host}/#{src}"
403
+ end
404
+
405
+ def authn
406
+ "#{@username}:#{@password}"
407
+ end
408
+
409
+ def curl_cmd(path, opts=[])
410
+ connection = Curl::Easy.new(url(path))
411
+ connection.http_auth_types = :basic
412
+ connection.username = @username
413
+ connection.password = @password
414
+
415
+ connection
416
+ end
417
+
418
+ def sftp_file_from_path(src)
419
+ file = ls(::File.dirname(src)).split("\n").map do |listing|
420
+ SftpFile.new(listing)
421
+ end.select do |file|
422
+ file.name == ::File.basename(src)
423
+ end
424
+
425
+ raise "#{src} not found" if file.empty?
426
+
427
+ file.first
428
+ end
429
+
430
+ def mkcommand(rd, wd, file, opts, size_hint: nil)
431
+ env = {}
432
+ cmd = [env, "curl"]
433
+
434
+ cmd << "-u"
435
+ cmd << authn
436
+ cmd << "-o"
437
+ cmd << "-"
438
+ cmd << "-N"
439
+ cmd << url(file)
440
+
441
+ if opts.include?('r')
442
+ cmd << {out: wd}
443
+ end
444
+
445
+ cmd
446
+ end
447
+
448
+ def with_readable(src, opts = 'r', &block)
449
+ raise "#{src} does not exist" unless exist?(src)
450
+
451
+ sftp_file = sftp_file_from_path(src)
452
+
453
+ mkio(src, opts, size_hint: sftp_file.size, &block)
454
+ end
455
+
456
+ def exist?(src)
457
+ files = ls(::File.dirname(src))
458
+ files.include?(::File.basename(src))
459
+ end
460
+
461
+ def ls(dir)
462
+ dir = dir + "/" unless "/" == dir[-1] # Trailing / makes curl list directory
463
+
464
+ return @dir_listings[dir] if @dir_listings.has_key?(dir)
465
+
466
+ listing = ''
467
+ connection = curl_cmd(dir)
468
+ connection.on_body { |data| listing << data; data.size }
469
+ connection.perform
470
+
471
+ @dir_listings[dir] = listing
472
+
473
+ listing
474
+ end
475
+
476
+ def stat(src)
477
+ sftp_file_from_path(src)
478
+ end
479
+ end
480
+
372
481
  class Mock < Filesystem
482
+ class MockStat
483
+ def initialize(io)
484
+ @io = io
485
+ end
486
+
487
+ def size
488
+ @io.respond_to?(:length) ? @io.length : 0
489
+ end
490
+ end
491
+
373
492
  def initialize(&new_io)
374
493
  @files = {}
375
494
  @dirs = {}
@@ -438,6 +557,10 @@ module Etna
438
557
  def exist?(src)
439
558
  @files.include?(src) || @dirs.include?(src)
440
559
  end
560
+
561
+ def stat(src)
562
+ @files[src].respond_to?(:stat) ? @files[src].stat : MockStat.new(@files[src])
563
+ end
441
564
  end
442
565
  end
443
566
  end
data/lib/etna/logger.rb CHANGED
@@ -40,6 +40,10 @@ module Etna
40
40
  error(trace)
41
41
  end
42
42
 
43
+ if defined? Yabeda
44
+ Yabeda.etna.rollbar_errors.increment({}, 1) rescue nil
45
+ end
46
+
43
47
  Rollbar.error(e)
44
48
  end
45
49
 
@@ -0,0 +1,38 @@
1
+ require "net/ssh"
2
+
3
+ module Etna
4
+ class RemoteSSH
5
+ class RemoteSSHError < Exception
6
+ end
7
+
8
+ def initialize(host:, username:, password: nil, port: 22, root:, **args)
9
+ @username = username
10
+ @password = password
11
+ @host = host
12
+ @port = port
13
+ @root = root
14
+ end
15
+
16
+ def ssh
17
+ @ssh ||= Net::SSH.start(@host, @username, password: @password)
18
+ end
19
+
20
+ def mkdir_p(dir)
21
+ output = ssh.exec!("mkdir -p #{dir}")
22
+
23
+ raise RemoteSSHError.new("Unable to mkdir -p, #{output}") unless 0 == output.exitstatus
24
+ end
25
+
26
+ def lftp_get(username:, password:, host:, remote_filename:, &block)
27
+ full_local_path = ::File.join(@root, host, remote_filename)
28
+ full_local_dir = ::File.dirname(full_local_path)
29
+ mkdir_p(full_local_dir)
30
+
31
+ cmd = "lftp sftp://#{username}:#{password}@#{host} -e \"get #{remote_filename} -o #{full_local_path}; bye\""
32
+
33
+ output = ssh.exec!(cmd)
34
+ raise RemoteSSHError.new("LFTP get failure: #{output}") unless 0 == output.exitstatus
35
+ yield remote_filename if block_given?
36
+ end
37
+ end
38
+ end
data/lib/etna/route.rb CHANGED
@@ -1,3 +1,6 @@
1
+ require 'digest'
2
+ require 'date'
3
+
1
4
  module Etna
2
5
  class Route
3
6
  attr_reader :name
@@ -58,6 +61,63 @@ module Etna
58
61
  end
59
62
 
60
63
  def call(app, request)
64
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
65
+
66
+ try_yabeda(request) do |tags|
67
+ Yabeda.etna.visits.increment(tags)
68
+ end
69
+
70
+ begin
71
+ process_call(app, request)
72
+ ensure
73
+ try_yabeda(request) do |tags|
74
+ dur = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
75
+ Yabeda.etna.response_time.measure(tags, dur)
76
+ end
77
+ end
78
+ end
79
+
80
+ def hash_user_email(email)
81
+ secret = Etna::Application.instance.config(:user_hash_secret) || 'notsosecret'
82
+ digest = email + secret + Date.today.to_s
83
+
84
+ if @name
85
+ digest += @name.to_s
86
+ else
87
+ digest += @route.to_s
88
+ end
89
+
90
+ Digest::MD5.hexdigest(digest)
91
+ end
92
+
93
+ def try_yabeda(request, &block)
94
+ if @action
95
+ controller, action = @action.split('#')
96
+ elsif @name
97
+ controller = "none"
98
+ action = @name
99
+ else
100
+ controller = "none"
101
+ action = @route
102
+ end
103
+
104
+ params = request.env['rack.request.params']
105
+ user = request.env['etna.user']
106
+ user_hash = user ? hash_user_email(user.email) : 'unknown'
107
+ project_name = "unknown"
108
+
109
+ if params && (params.include?(:project_name) || params.include?('project_name'))
110
+ project_name = params[:project_name] || params['project_name']
111
+ end
112
+
113
+ begin
114
+ block.call({ controller: controller, action: action, user_hash: user_hash, project_name: project_name })
115
+ rescue => e
116
+ raise e unless Etna::Application.instance.environment == :production
117
+ end
118
+ end
119
+
120
+ def process_call(app, request)
61
121
  update_params(request)
62
122
 
63
123
  unless authorized?(request)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: etna
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.37
4
+ version: 0.1.41
5
5
  platform: ruby
6
6
  authors:
7
7
  - Saurabh Asthana
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-05-27 00:00:00.000000000 Z
11
+ date: 2021-08-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -94,6 +94,34 @@ dependencies:
94
94
  - - ">="
95
95
  - !ruby/object:Gem::Version
96
96
  version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: curb
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: net-ssh
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
97
125
  description: See summary
98
126
  email: Saurabh.Asthana@ucsf.edu
99
127
  executables:
@@ -144,6 +172,7 @@ files:
144
172
  - lib/etna/clients/metis/client.rb
145
173
  - lib/etna/clients/metis/models.rb
146
174
  - lib/etna/clients/metis/workflows.rb
175
+ - lib/etna/clients/metis/workflows/ingest_metis_data_workflow.rb
147
176
  - lib/etna/clients/metis/workflows/metis_download_workflow.rb
148
177
  - lib/etna/clients/metis/workflows/metis_upload_workflow.rb
149
178
  - lib/etna/clients/metis/workflows/sync_metis_data_workflow.rb
@@ -171,6 +200,7 @@ files:
171
200
  - lib/etna/metrics.rb
172
201
  - lib/etna/multipart_serializable_nested_hash.rb
173
202
  - lib/etna/parse_body.rb
203
+ - lib/etna/remote.rb
174
204
  - lib/etna/route.rb
175
205
  - lib/etna/server.rb
176
206
  - lib/etna/sign_service.rb