etna 0.1.36 → 0.1.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a6d9abd6b64884e701b1055215018b85939ee0bf6cc19275eccae0ebffe92856
4
- data.tar.gz: a382140c854224855b317923551d467baa3b29202ec6ebccc2fb8d7782115d06
3
+ metadata.gz: e83d3c30c23693011d59b103f8b5a9372950eba1b3123376a288826204d47c62
4
+ data.tar.gz: c3b50952e71d91f4361a87e872ba5532849644abe42fbc9e480549726a6bbf93
5
5
  SHA512:
6
- metadata.gz: e150a0e7dac99e3c637151033d4105617910188f9c61c8e2812b2fa75c3d3967e95bb2fa43babb98802b14640658e02167c81b825fa0e1be41cef6340a74d4fa
7
- data.tar.gz: 6f7365324ceed077161b4507397380837cc0d355f6cbd4dfff22b2abb527eaeafc2455e3cb3138d7b9a8085d3b4a2b3cf05b86417f2458a9bc80da859c77ed24
6
+ metadata.gz: c3c561cdd7db4631c267f2edba8421b94baba5de790ca1ff9edd06cd21d039d7224d868eaa43ba7090b4114b595830ac777c1c4688ec15c9320cde90530be4e5
7
+ data.tar.gz: 7148582a82f7e90357ed6658c7337bce7b34dd532b4a899ac27a2d74f40f34224cb6c225b2aa4db4164c467aa8a5c189d7acf28a1c462097e619e9875fbdc4d1
data/lib/etna.rb CHANGED
@@ -21,6 +21,7 @@ require_relative './etna/environment_scoped'
21
21
  require_relative './etna/filesystem'
22
22
  require_relative './etna/formatting'
23
23
  require_relative './etna/cwl'
24
+ require_relative './etna/metrics'
24
25
 
25
26
  class EtnaApp
26
27
  include Etna::Application
@@ -8,6 +8,7 @@ require_relative './command'
8
8
  require_relative './generate_autocompletion_script'
9
9
  require 'singleton'
10
10
  require 'rollbar'
11
+ require 'fileutils'
11
12
 
12
13
  module Etna::Application
13
14
  def self.included(other)
@@ -59,6 +60,72 @@ module Etna::Application
59
60
  end
60
61
  end
61
62
 
63
+ # This will cause metrics to persist to a file.
64
+ # NOTE -- /tmp/metrics.bin should be a persistent mount when using this.
65
+ # You will still need to export metrics in the text format for the node_exporter on the host machine to
66
+ # export them to prometheus. Ensure that the /tmp/metrics.bin is on a named volume or a bind mount, either is fine.
67
+ def enable_job_metrics!
68
+ require 'prometheus'
69
+ Prometheus::Client.config.data_store = Prometheus::Client::DataStores::DirectFileStore.new({
70
+ dir: "/tmp/metrics.bin"
71
+ })
72
+ end
73
+
74
+ def setup_yabeda
75
+ application = self.id
76
+ Yabeda.configure do
77
+ default_tag :application, application
78
+
79
+ group :etna do
80
+ histogram :response_time do
81
+ comment "Time spent by a controller returning a response"
82
+ unit :seconds
83
+ tags [:controller, :action, :user_hash, :project_name]
84
+ buckets [0.01, 0.1, 0.3, 0.5, 1, 5]
85
+ end
86
+
87
+ counter :visits do
88
+ comment "Counts visits to the controller"
89
+ tags [:controller, :action, :user_hash, :project_name]
90
+ end
91
+
92
+ counter :rollbar_errors do
93
+ comment "Counts errors detected by and sent to rollbar"
94
+ end
95
+
96
+ gauge :last_command_completion do
97
+ comment "Unix time of last time command was completed"
98
+ tags [:command, :status, :application]
99
+ end
100
+
101
+ histogram :command_runtime do
102
+ comment "Time spent processing a given command"
103
+ tags [:command, :status, :application]
104
+ unit :seconds
105
+ buckets [0.1, 1, 5, 60, 300, 1500]
106
+ end
107
+ end
108
+ end
109
+
110
+ Yabeda.configure!
111
+ end
112
+
113
+ # Writes all metrics currently gathered to a text format prometheus file. If /tmp/metrics.prom is bind mounted
114
+ # to the host directed bound to the node_exporter's file exporter directory, these will be exported to
115
+ # prometheus. Combine this enable_job_metrics! for maximum effect.
116
+ def write_job_metrics(name)
117
+ node_metrics_dir = "/tmp/metrics.prom"
118
+ ::FileUtils.mkdir_p(node_metrics_dir)
119
+
120
+ tmp_file = ::File.join(node_metrics_dir, "#{name}.prom.$$")
121
+ ::File.open(tmp_file, "w") do |f|
122
+ f.write(Prometheus::Client::Formats::Text.marshal(Prometheus::Client.registry))
123
+ end
124
+
125
+ require 'fileutils'
126
+ ::FileUtils.mv(tmp_file, ::File.join(node_metrics_dir, "#{name}.prom"))
127
+ end
128
+
62
129
  def setup_logger
63
130
  @logger = Etna::Logger.new(
64
131
  # The name of the log_file, required.
@@ -108,17 +175,32 @@ module Etna::Application
108
175
  end
109
176
 
110
177
  def run_command(config, *args, &block)
178
+ application = self.id
179
+ status = 'success'
180
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
111
181
  cmd, cmd_args, cmd_kwds = find_command(*args)
112
- cmd.setup(config)
113
182
 
114
- if block_given?
115
- return unless yield [cmd, cmd_args]
116
- end
183
+ begin
184
+ cmd.setup(config)
185
+ if block_given?
186
+ return unless yield [cmd, cmd_args]
187
+ end
117
188
 
118
- cmd.execute(*cmd.fill_in_missing_params(cmd_args), **cmd_kwds)
119
- rescue => e
120
- Rollbar.error(e)
121
- raise
189
+ cmd.execute(*cmd.fill_in_missing_params(cmd_args), **cmd_kwds)
190
+ rescue => e
191
+ Rollbar.error(e)
192
+ status = 'failed'
193
+ raise
194
+ ensure
195
+ if defined?(Yabeda) && Yabeda.configured?
196
+ tags = { command: cmd.class.name, status: status, application: application }
197
+ dur = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
198
+
199
+ Yabeda.etna.last_command_completion.set(tags, Time.now.to_i)
200
+ Yabeda.etna.command_runtime.measure(tags, dur)
201
+ write_job_metrics("run_command")
202
+ end
203
+ end
122
204
  end
123
205
  end
124
206
 
@@ -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,31 @@
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)
13
+ srcs.each do |src|
14
+ next unless ingest_filesystem.exist?(src)
15
+
16
+ logger&.info("Copying file #{src} (#{Etna::Formatting.as_size(ingest_filesystem.stat(src).size)})")
17
+
18
+ # For ingestion triage, just copy over the exact path + filename.
19
+ copy_file(dest: src, src: src)
20
+ end
21
+ end
22
+
23
+ def copy_file(dest:, src:)
24
+ ingest_filesystem.with_readable(src, "r") do |file|
25
+ metis_filesystem.do_streaming_upload(file, dest, file.size)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ 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)
@@ -52,7 +52,12 @@ class DirectedGraph
52
52
  result[grandparent] << child_node if result.include?(grandparent)
53
53
  end
54
54
 
55
- result[child_node] = []
55
+ # Depending on the graph shape, diamonds could lead to
56
+ # resetting of previously calculated dependencies.
57
+ # Here we avoid resetting existing entries in `result`
58
+ # and instead concatenate them if they already exist.
59
+ result[child_node] = [] unless result.include?(child_node)
60
+ result[n].concat(result[child_node]) if result.include?(child_node) && result.include?(n)
56
61
  end
57
62
  end
58
63
 
@@ -3,6 +3,8 @@ require 'fileutils'
3
3
  require 'open3'
4
4
  require 'securerandom'
5
5
  require 'concurrent-ruby'
6
+ require 'net/sftp'
7
+ require 'net/ssh'
6
8
 
7
9
  module Etna
8
10
  # A class that encapsulates opening / reading file system entries that abstracts normal file access in order
@@ -50,6 +52,11 @@ module Etna
50
52
  ::FileUtils.mv(src, dest)
51
53
  end
52
54
 
55
+ def stat(src)
56
+ raise "stat not supported by #{self.class.name}" unless self.class == Filesystem
57
+ ::File.stat(src)
58
+ end
59
+
53
60
  class EmptyIO < StringIO
54
61
  def write(*args)
55
62
  # Do nothing -- always leave empty
@@ -369,7 +376,59 @@ module Etna
369
376
  end
370
377
  end
371
378
 
379
+ class SftpFilesystem < Filesystem
380
+ def initialize(host:, username:, password: nil, port: 22, **args)
381
+ @username = username
382
+ @password = password
383
+ @host = host
384
+ @port = port
385
+ end
386
+
387
+ def ssh
388
+ @ssh ||= Net::SSH.start(@host, @username, password: @password)
389
+ end
390
+
391
+ def sftp
392
+ @sftp ||= begin
393
+ conn = Net::SFTP::Session.new(ssh)
394
+ conn.loop { conn.opening? }
395
+
396
+ conn
397
+ end
398
+ end
399
+
400
+ def with_readable(src, opts = 'r', &block)
401
+ sftp.file.open(src, opts, &block)
402
+ end
403
+
404
+ def ls(dir)
405
+ sftp.dir.entries(dir)
406
+ end
407
+
408
+ def exist?(src)
409
+ begin
410
+ sftp.file.open(src)
411
+ rescue Net::SFTP::StatusException
412
+ return false
413
+ end
414
+ return true
415
+ end
416
+
417
+ def stat(src)
418
+ sftp.file.open(src).stat
419
+ end
420
+ end
421
+
372
422
  class Mock < Filesystem
423
+ class MockStat
424
+ def initialize
425
+ end
426
+
427
+ def size
428
+ 0
429
+ end
430
+ end
431
+
373
432
  def initialize(&new_io)
374
433
  @files = {}
375
434
  @dirs = {}
@@ -438,6 +497,10 @@ module Etna
438
497
  def exist?(src)
439
498
  @files.include?(src) || @dirs.include?(src)
440
499
  end
500
+
501
+ def stat(src)
502
+ @files[src].respond_to?(:stat) ? @files[src].stat : MockStat.new
503
+ end
441
504
  end
442
505
  end
443
506
  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,26 @@
1
+
2
+ module Etna
3
+ class MetricsExporter
4
+ def initialize(app, path: '/metrics')
5
+ @app = app
6
+ @path = path
7
+ end
8
+
9
+ def exporter
10
+ @exporter ||= begin
11
+ exporter = Yabeda::Prometheus::Exporter.new(@app, path: @path)
12
+ Rack::Auth::Basic.new(exporter) do |user, pw|
13
+ user == 'prometheus' && pw == ENV['METRICS_PW']
14
+ end
15
+ end
16
+ end
17
+
18
+ def call(env)
19
+ if env['PATH_INFO'] == @path
20
+ exporter.call(env)
21
+ else
22
+ @app.call(env)
23
+ end
24
+ end
25
+ end
26
+ 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)
@@ -148,7 +208,10 @@ module Etna
148
208
  params = request.env['rack.request.params']
149
209
 
150
210
  @auth[:user].all? do |constraint, param_name|
151
- user.respond_to?(constraint) && user.send(constraint, params[param_name])
211
+ user.respond_to?(constraint) && (
212
+ param_name.is_a?(Symbol) ?
213
+ user.send(constraint, params[param_name]) :
214
+ user.send(constraint, param_name))
152
215
  end
153
216
  end
154
217
 
data/lib/etna/server.rb CHANGED
@@ -74,6 +74,11 @@ module Etna
74
74
  def initialize
75
75
  # Setup logging.
76
76
  application.setup_logger
77
+
78
+ # This needs to be required before yabeda invocation, but cannot belong at the top of any module since clients
79
+ # do not install yabeda.
80
+ require 'yabeda'
81
+ application.setup_yabeda
77
82
  end
78
83
 
79
84
  private
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.36
4
+ version: 0.1.40
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-10 00:00:00.000000000 Z
11
+ date: 2021-07-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -94,6 +94,20 @@ dependencies:
94
94
  - - ">="
95
95
  - !ruby/object:Gem::Version
96
96
  version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: net-sftp
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: 3.0.0
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: 3.0.0
97
111
  description: See summary
98
112
  email: Saurabh.Asthana@ucsf.edu
99
113
  executables:
@@ -144,6 +158,7 @@ files:
144
158
  - lib/etna/clients/metis/client.rb
145
159
  - lib/etna/clients/metis/models.rb
146
160
  - lib/etna/clients/metis/workflows.rb
161
+ - lib/etna/clients/metis/workflows/ingest_metis_data_workflow.rb
147
162
  - lib/etna/clients/metis/workflows/metis_download_workflow.rb
148
163
  - lib/etna/clients/metis/workflows/metis_upload_workflow.rb
149
164
  - lib/etna/clients/metis/workflows/sync_metis_data_workflow.rb
@@ -168,6 +183,7 @@ files:
168
183
  - lib/etna/hmac.rb
169
184
  - lib/etna/json_serializable_struct.rb
170
185
  - lib/etna/logger.rb
186
+ - lib/etna/metrics.rb
171
187
  - lib/etna/multipart_serializable_nested_hash.rb
172
188
  - lib/etna/parse_body.rb
173
189
  - lib/etna/route.rb