etna 0.1.39 → 0.1.43

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: c21212f38bbad3cf757eb84a9e453428c9d0b6f195fd34eb81f98aeff2746490
4
- data.tar.gz: 3581821244c087cbbfbac162f9a5c61911e06ab64923c603a144d36ca3bedff0
3
+ metadata.gz: 2e30f0d83fdf8f5781f723080ddd042d371f3131a415c85ec441b56178cef27c
4
+ data.tar.gz: a543a3464ca34e0d6daedddbf702cf067f7dfdcbdc972e4e9a9a626118d5fbbd
5
5
  SHA512:
6
- metadata.gz: 3289530a2e874d2b79b0da66ea787e30b335b7a60626e4b67f1be9919067d4b79752db4e878ca8851dbce6c68a1e59a573bcbc60314bb7a35268cd37cc2eeaae
7
- data.tar.gz: 2758348f253992e08fbf589d2b5444844becf69e179872c630804a7e8885568189168edd056c56beabe4ed5dc3e0905f46c51595896bd7bdd1255fbe4ece71ec
6
+ metadata.gz: d11b9f2882e7499decb4854c5c399bce24e31b14ff52c83006708bd4f83706e4a754d2c96838241b1885ae2ad4f2eb360a0f815b38f5009f1a75fb4fe7246275
7
+ data.tar.gz: 3447fa61b285fdbe78e5e0646b36d489f3fd2677cce2415ebe001f8b691ddc281b9514a12bd9424ed9023f0b58609482edba77156066377ab1840e06bf359a5a
data/etna.completion CHANGED
@@ -84,7 +84,7 @@ arg_flag_completion_names="$arg_flag_completion_names "
84
84
  multi_flags="$multi_flags "
85
85
  while [[ "$#" != "0" ]]; do
86
86
  if [[ "$#" == "1" ]]; then
87
- all_completion_names="apply_template attributes copy_template help load_from_redcap"
87
+ all_completion_names="apply_template attributes copy_template help load_from_redcap set_date_shift_root"
88
88
  all_completion_names="$all_completion_names $all_flag_completion_names"
89
89
  if [[ -z "$(echo $all_completion_names | xargs)" ]]; then
90
90
  return
@@ -568,6 +568,54 @@ return
568
568
  fi
569
569
  done
570
570
  return
571
+ elif [[ "$1" == "set_date_shift_root" ]]; then
572
+ shift
573
+ if [[ "$#" == "1" ]]; then
574
+ all_completion_names="__project_name__"
575
+ if [[ -z "$(echo $all_completion_names | xargs)" ]]; then
576
+ return
577
+ fi
578
+ COMPREPLY=($(compgen -W "$all_completion_names" -- "$1"))
579
+ return
580
+ fi
581
+ shift
582
+ all_flag_completion_names="$all_flag_completion_names --date-shift-root --target-model "
583
+ arg_flag_completion_names="$arg_flag_completion_names --target-model "
584
+ multi_flags="$multi_flags "
585
+ declare _completions_for_target_model="__target_model__"
586
+ while [[ "$#" != "0" ]]; do
587
+ if [[ "$#" == "1" ]]; then
588
+ all_completion_names=""
589
+ all_completion_names="$all_completion_names $all_flag_completion_names"
590
+ if [[ -z "$(echo $all_completion_names | xargs)" ]]; then
591
+ return
592
+ fi
593
+ COMPREPLY=($(compgen -W "$all_completion_names" -- "$1"))
594
+ return
595
+ elif [[ -z "$(echo $all_flag_completion_names | xargs)" ]]; then
596
+ return
597
+ elif [[ "$all_flag_completion_names" =~ $1\ ]]; then
598
+ if ! [[ "$multi_flags" =~ $1\ ]]; then
599
+ all_flag_completion_names="${all_flag_completion_names//$1\ /}"
600
+ fi
601
+ a=$1
602
+ shift
603
+ if [[ "$arg_flag_completion_names" =~ $a\ ]]; then
604
+ if [[ "$#" == "1" ]]; then
605
+ a="${a//--/}"
606
+ a="${a//-/_}"
607
+ i="_completions_for_$a"
608
+ all_completion_names="${!i}"
609
+ COMPREPLY=($(compgen -W "$all_completion_names" -- "$1"))
610
+ return
611
+ fi
612
+ shift
613
+ fi
614
+ else
615
+ return
616
+ fi
617
+ done
618
+ return
571
619
  elif [[ -z "$(echo $all_flag_completion_names | xargs)" ]]; then
572
620
  return
573
621
  elif [[ "$all_flag_completion_names" =~ $1\ ]]; then
@@ -738,10 +786,11 @@ COMPREPLY=($(compgen -W "$all_completion_names" -- "$1"))
738
786
  return
739
787
  elif [[ "$1" == "generate" ]]; then
740
788
  shift
741
- all_flag_completion_names="$all_flag_completion_names --task --project-name "
742
- arg_flag_completion_names="$arg_flag_completion_names --project-name "
789
+ all_flag_completion_names="$all_flag_completion_names --task --project-name --email "
790
+ arg_flag_completion_names="$arg_flag_completion_names --project-name --email "
743
791
  multi_flags="$multi_flags "
744
792
  declare _completions_for_project_name="__project_name__"
793
+ declare _completions_for_email="__email__"
745
794
  while [[ "$#" != "0" ]]; do
746
795
  if [[ "$#" == "1" ]]; then
747
796
  all_completion_names=""
data/lib/commands.rb CHANGED
@@ -411,6 +411,25 @@ class EtnaApp
411
411
  end
412
412
  end
413
413
  end
414
+
415
+ class SetDateShiftRoot < Etna::Command
416
+ include WithEtnaClients
417
+ include WithLogger
418
+ include StrongConfirmation
419
+
420
+ boolean_flags << '--date-shift-root'
421
+ string_flags << '--target-model'
422
+
423
+ def execute(project_name, target_model: 'subject', date_shift_root: false)
424
+ magma_client.update_model(Etna::Clients::Magma::UpdateModelRequest.new(
425
+ actions:[Etna::Clients::Magma::SetDateShiftRootAction.new(
426
+ model_name: target_model,
427
+ date_shift_root: date_shift_root
428
+ )],
429
+ project_name: project_name,
430
+ ))
431
+ end
432
+ end
414
433
  end
415
434
  end
416
435
 
@@ -53,6 +53,21 @@ module Etna::Application
53
53
  def configure(opts)
54
54
  @config = opts
55
55
 
56
+ # Apply environmental variables of the form "APP__x__y__z"
57
+ prefix = "#{self.class.name.upcase}__"
58
+ ENV.keys.select { |k| k.start_with?(prefix) }.each do |key|
59
+ path = key.split("__", -1)
60
+ path.shift # drop the first, just app name
61
+
62
+ target = @config
63
+ while path.length > 1
64
+ n = path.shift
65
+ target = (target[n.downcase.to_sym] ||= {})
66
+ end
67
+
68
+ target[path.last.downcase.to_sym] ||= ENV[key]
69
+ end
70
+
56
71
  if (rollbar_config = config(:rollbar)) && rollbar_config[:access_token]
57
72
  Rollbar.configure do |config|
58
73
  config.access_token = rollbar_config[:access_token]
@@ -60,17 +75,6 @@ module Etna::Application
60
75
  end
61
76
  end
62
77
 
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
78
  def setup_yabeda
75
79
  application = self.id
76
80
  Yabeda.configure do
@@ -192,13 +196,14 @@ module Etna::Application
192
196
  status = 'failed'
193
197
  raise
194
198
  ensure
195
- if Yabeda.configured?
199
+ if defined?(Yabeda) && Yabeda.configured?
196
200
  tags = { command: cmd.class.name, status: status, application: application }
197
201
  dur = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
198
202
 
199
203
  Yabeda.etna.last_command_completion.set(tags, Time.now.to_i)
200
204
  Yabeda.etna.command_runtime.measure(tags, dur)
201
- write_job_metrics("run_command")
205
+
206
+ write_job_metrics("run_command.#{cmd.class.name}")
202
207
  end
203
208
  end
204
209
  end
@@ -0,0 +1,36 @@
1
+ module Etna
2
+ class Censor
3
+ def initialize(log_redact_keys)
4
+ @log_redact_keys = log_redact_keys
5
+ end
6
+
7
+ def redact_keys
8
+ @log_redact_keys
9
+ end
10
+
11
+ def redact(key, value)
12
+ # Redact any values for the supplied key values, so they
13
+ # don't appear in the logs.
14
+ return compact(value) unless redact_keys
15
+
16
+ if redact_keys.include?(key)
17
+ return "*"
18
+ elsif value.is_a?(Hash)
19
+ redacted_value = value.map do |value_key, value_value|
20
+ [value_key, redact(value_key, value_value)]
21
+ end.to_h
22
+ return redacted_value
23
+ end
24
+
25
+ return compact(value)
26
+ end
27
+
28
+ private
29
+
30
+ def compact(value)
31
+ value = value.to_s
32
+ value = value[0..500] + "..." + value[-100..-1] if value.length > 600
33
+ value
34
+ end
35
+ end
36
+ end
@@ -58,11 +58,19 @@ module Etna
58
58
  end
59
59
  end
60
60
 
61
- class AddModelAction < Struct.new(:action_name, :model_name, :parent_model_name, :parent_link_type, :identifier, keyword_init: true)
61
+ class AddModelAction < Struct.new(:action_name, :model_name, :parent_model_name, :parent_link_type, :identifier, :date_shift_root, keyword_init: true)
62
62
  include JsonSerializableStruct
63
63
 
64
64
  def initialize(**args)
65
- super({action_name: 'add_model'}.update(args))
65
+ super({action_name: 'add_model', date_shift_root: false}.update(args))
66
+ end
67
+ end
68
+
69
+ class SetDateShiftRootAction < Struct.new(:action_name, :model_name, :date_shift_root, keyword_init: true)
70
+ include JsonSerializableStruct
71
+
72
+ def initialize(**args)
73
+ super({action_name: 'set_date_shift_root'}.update(args))
66
74
  end
67
75
  end
68
76
 
@@ -620,6 +628,7 @@ module Etna
620
628
  MATRIX = AttributeType.new("matrix")
621
629
  PARENT = AttributeType.new("parent")
622
630
  TABLE = AttributeType.new("table")
631
+ SHIFTED_DATE_TIME = AttributeType.new("shifted_date_time")
623
632
  end
624
633
 
625
634
  class ParentLinkType < String
@@ -28,6 +28,21 @@ module Etna
28
28
  @etna_client.folder_list(list_folder_request.to_h))
29
29
  end
30
30
 
31
+ def list_folder_by_id(list_folder_by_id_request = ListFolderByIdRequest.new)
32
+ FoldersAndFilesResponse.new(
33
+ @etna_client.folder_list_by_id(list_folder_by_id_request.to_h))
34
+ end
35
+
36
+ def touch_folder(touch_folder_request = TouchFolderRequest.new)
37
+ FoldersResponse.new(
38
+ @etna_client.folder_touch(touch_folder_request.to_h))
39
+ end
40
+
41
+ def touch_file(touch_file_request = TouchFileRequest.new)
42
+ FilesResponse.new(
43
+ @etna_client.file_touch(touch_file_request.to_h))
44
+ end
45
+
31
46
  def ensure_parent_folder_exists(project_name:, bucket_name:, path:)
32
47
  create_folder_request = CreateFolderRequest.new(
33
48
  project_name: project_name,
@@ -181,7 +196,16 @@ module Etna
181
196
 
182
197
  return if found_folders.length == 0
183
198
 
184
- found_folders.each { |folder|
199
+ rename_folders(
200
+ project_name: project_name,
201
+ source_bucket: source_bucket,
202
+ source_folders: found_folders,
203
+ dest_bucket: dest_bucket
204
+ )
205
+ end
206
+
207
+ def rename_folders(project_name:, source_bucket:, source_folders:, dest_bucket:)
208
+ source_folders.each { |folder|
185
209
  # If the destination folder already exists, we need to copy the files
186
210
  # over to it and delete the source folder.
187
211
  create_folder_request = CreateFolderRequest.new(
@@ -210,34 +234,30 @@ module Etna
210
234
  }
211
235
  end
212
236
 
213
- def recursively_rename_folder(project_name:, source_bucket:, dest_bucket:, folder:)
214
- folder_contents = list_folder(
215
- Etna::Clients::Metis::ListFolderRequest.new(
216
- project_name: project_name,
217
- bucket_name: source_bucket,
218
- folder_path: folder.folder_path
219
- ))
237
+ def resolve_conflicts_and_verify_rename(project_name:, source_bucket:, dest_bucket:, folder:, file:)
238
+ parent_folder = ::File.dirname(file.file_path)
220
239
 
221
- folder_contents.folders.all.each do |sub_folder|
222
- recursively_rename_folder(
223
- project_name: project_name,
224
- source_bucket: source_bucket,
225
- dest_bucket: dest_bucket,
226
- folder: sub_folder
227
- )
228
- end
240
+ should_rename_original = true
229
241
 
230
- folder_contents.files.all.each do |file|
242
+ # If the destination folder already exists, check to see if
243
+ # the file also exists, otherwise we risk a
244
+ # rename conflict.
245
+ create_folder_request = CreateFolderRequest.new(
246
+ project_name: project_name,
247
+ bucket_name: dest_bucket,
248
+ folder_path: parent_folder
249
+ )
250
+
251
+ if folder_exists?(create_folder_request)
231
252
  # If file exists in destination, delete the older file.
232
253
  list_dest_folder_request = Etna::Clients::Metis::ListFolderRequest.new(
233
254
  bucket_name: dest_bucket,
234
255
  project_name: project_name,
235
- folder_path: ::File.dirname(file.file_path)
256
+ folder_path: parent_folder
236
257
  )
237
258
 
238
259
  dest_file = list_folder(list_dest_folder_request).files.all.find { |f| f.file_name == file.file_name }
239
260
 
240
- should_rename = true
241
261
  if (dest_file && file.updated_at <= dest_file.updated_at)
242
262
  # Delete source file if it's out of date
243
263
  delete_file(Etna::Clients::Metis::DeleteFileRequest.new(
@@ -246,7 +266,7 @@ module Etna
246
266
  file_path: file.file_path,
247
267
  ))
248
268
 
249
- should_rename = false
269
+ should_rename_original = false
250
270
  elsif (dest_file && file.updated_at > dest_file.updated_at)
251
271
  # Delete dest file if it's out of date
252
272
  delete_file(Etna::Clients::Metis::DeleteFileRequest.new(
@@ -255,17 +275,44 @@ module Etna
255
275
  file_path: dest_file.file_path,
256
276
  ))
257
277
  end
278
+ end
258
279
 
259
- if should_rename
260
- rename_file(Etna::Clients::Metis::RenameFileRequest.new(
261
- bucket_name: source_bucket,
262
- project_name: project_name,
263
- file_path: file.file_path,
264
- new_bucket_name: dest_bucket,
265
- new_file_path: file.file_path,
266
- create_parent: true)
267
- )
268
- end
280
+ should_rename_original
281
+ end
282
+
283
+ def recursively_rename_folder(project_name:, source_bucket:, dest_bucket:, folder:)
284
+ folder_contents = list_folder(
285
+ Etna::Clients::Metis::ListFolderRequest.new(
286
+ project_name: project_name,
287
+ bucket_name: source_bucket,
288
+ folder_path: folder.folder_path
289
+ ))
290
+
291
+ folder_contents.folders.all.each do |sub_folder|
292
+ recursively_rename_folder(
293
+ project_name: project_name,
294
+ source_bucket: source_bucket,
295
+ dest_bucket: dest_bucket,
296
+ folder: sub_folder
297
+ )
298
+ end
299
+
300
+ folder_contents.files.all.each do |file|
301
+ should_rename = resolve_conflicts_and_verify_rename(
302
+ project_name: project_name,
303
+ source_bucket: source_bucket,
304
+ dest_bucket: dest_bucket,
305
+ folder: folder,
306
+ file: file)
307
+
308
+ rename_file(Etna::Clients::Metis::RenameFileRequest.new(
309
+ bucket_name: source_bucket,
310
+ project_name: project_name,
311
+ file_path: file.file_path,
312
+ new_bucket_name: dest_bucket,
313
+ new_file_path: file.file_path,
314
+ create_parent: true)
315
+ ) if should_rename
269
316
  end
270
317
 
271
318
  # Now delete the source folder
@@ -65,6 +65,51 @@ module Etna
65
65
  end
66
66
  end
67
67
 
68
+ class ListFolderByIdRequest < Struct.new(:project_name, :bucket_name, :folder_id, keyword_init: true)
69
+ include JsonSerializableStruct
70
+
71
+ def initialize(**params)
72
+ super({}.update(params))
73
+ end
74
+
75
+ def to_h
76
+ # The :project_name comes in from Polyphemus as a symbol value,
77
+ # we need to make sure it's a string because it's going
78
+ # in the URL.
79
+ super().compact.transform_values(&:to_s)
80
+ end
81
+ end
82
+
83
+ class TouchFolderRequest < Struct.new(:project_name, :bucket_name, :folder_path, keyword_init: true)
84
+ include JsonSerializableStruct
85
+
86
+ def initialize(**params)
87
+ super({}.update(params))
88
+ end
89
+
90
+ def to_h
91
+ # The :project_name comes in from Polyphemus as a symbol value,
92
+ # we need to make sure it's a string because it's going
93
+ # in the URL.
94
+ super().compact.transform_values(&:to_s)
95
+ end
96
+ end
97
+
98
+ class TouchFileRequest < Struct.new(:project_name, :bucket_name, :file_path, keyword_init: true)
99
+ include JsonSerializableStruct
100
+
101
+ def initialize(**params)
102
+ super({}.update(params))
103
+ end
104
+
105
+ def to_h
106
+ # The :project_name comes in from Polyphemus as a symbol value,
107
+ # we need to make sure it's a string because it's going
108
+ # in the URL.
109
+ super().compact.transform_values(&:to_s)
110
+ end
111
+ end
112
+
68
113
  class CreateFolderRequest < Struct.new(:project_name, :bucket_name, :folder_path, keyword_init: true)
69
114
  include JsonSerializableStruct
70
115
 
@@ -110,11 +155,11 @@ module Etna
110
155
  end
111
156
  end
112
157
 
113
- class FindRequest < Struct.new(:project_name, :bucket_name, :limit, :offset, :params, keyword_init: true)
158
+ class FindRequest < Struct.new(:project_name, :bucket_name, :limit, :offset, :params, :hide_paths, keyword_init: true)
114
159
  include JsonSerializableStruct
115
160
 
116
161
  def initialize(**args)
117
- super({params: []}.update(args))
162
+ super({params: [], hide_paths: false}.update(args))
118
163
  end
119
164
 
120
165
  def add_param(param)
@@ -126,6 +171,17 @@ module Etna
126
171
  # easier to do from a JSON string
127
172
  JSON.parse(to_json, :symbolize_names => true)
128
173
  end
174
+
175
+ def clone
176
+ FindRequest.new(
177
+ project_name: self.project_name,
178
+ bucket_name: self.bucket_name,
179
+ limit: self.limit,
180
+ offset: self.offset,
181
+ params: self.params.dup,
182
+ hide_paths: self.hide_paths
183
+ )
184
+ end
129
185
  end
130
186
 
131
187
  class FindParam < Struct.new(:attribute, :predicate, :value, :type, keyword_init: true)
@@ -225,6 +281,13 @@ module Etna
225
281
  @raw = raw
226
282
  end
227
283
 
284
+ def with_containing_folder(folder)
285
+ folder_path = folder.is_a?(Folder) ? folder.folder_path : folder
286
+ File.new({}.update(self.raw).update({
287
+ file_path: ::File.join(folder_path, self.file_name)
288
+ }))
289
+ end
290
+
228
291
  def file_path
229
292
  raw[:file_path]
230
293
  end
@@ -259,6 +322,14 @@ module Etna
259
322
  def size
260
323
  raw[:size]
261
324
  end
325
+
326
+ def file_hash
327
+ raw[:file_hash]
328
+ end
329
+
330
+ def folder_id
331
+ raw[:folder_id]
332
+ end
262
333
  end
263
334
 
264
335
  class Folder
@@ -279,6 +350,19 @@ module Etna
279
350
  def bucket_name
280
351
  raw[:bucket_name]
281
352
  end
353
+
354
+ def project_name
355
+ raw[:project_name]
356
+ end
357
+
358
+ def updated_at
359
+ time = raw[:updated_at]
360
+ time.nil? ? nil : Time.parse(time)
361
+ end
362
+
363
+ def id
364
+ raw[:id]
365
+ end
282
366
  end
283
367
 
284
368
  class AuthorizeUploadRequest < Struct.new(:project_name, :bucket_name, :file_path, keyword_init: true)
@@ -9,20 +9,24 @@ module Etna
9
9
  # Since we are doing manual triage of files,
10
10
  # do not automatically copy directory trees.
11
11
  # srcs must be a list of full paths to files.
12
- def copy_files(srcs)
12
+ def copy_files(srcs, &block)
13
13
  srcs.each do |src|
14
- next unless ingest_filesystem.exist?(src)
14
+ if !ingest_filesystem.exist?(src)
15
+ logger&.warn("#{src} does not exist on source filesystem. Skipping.")
16
+ next
17
+ end
15
18
 
16
19
  logger&.info("Copying file #{src} (#{Etna::Formatting.as_size(ingest_filesystem.stat(src).size)})")
17
20
 
18
21
  # For ingestion triage, just copy over the exact path + filename.
19
- copy_file(dest: src, src: src)
22
+ copy_file(dest: src, src: src, &block)
20
23
  end
21
24
  end
22
25
 
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
+ 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?
26
30
  end
27
31
  end
28
32
  end
@@ -110,8 +110,6 @@ module Etna
110
110
 
111
111
  def initialize(source_file: nil, next_blob_size: nil, current_byte_position: nil)
112
112
  self.source_file = source_file
113
- self.next_blob_size = next_blob_size
114
- self.current_byte_position = current_byte_position
115
113
  self.next_blob_size = [file_size, INITIAL_BLOB_SIZE].min
116
114
  self.current_byte_position = 0
117
115
  end
@@ -42,7 +42,10 @@ module Etna
42
42
 
43
43
  [501, {}, ['This controller is not implemented.']]
44
44
  rescue Exception => e
45
- handle_error(e)
45
+ error = e
46
+ ensure
47
+ log_request
48
+ return handle_error(error) if error
46
49
  end
47
50
 
48
51
  def require_params(*params)
@@ -82,8 +85,36 @@ module Etna
82
85
  @response.finish
83
86
  end
84
87
 
88
+ def config_hosts
89
+ [:janus, :magma, :timur, :metis, :vulcan, :polyphemus].map do |host|
90
+ [ :"#{host}_host", @server.send(:application).config(host)&.dig(:host) ]
91
+ end.to_h.compact
92
+ end
93
+
85
94
  private
86
95
 
96
+ def redact_keys
97
+ @request.env['etna.redact_keys']
98
+ end
99
+
100
+ def add_redact_keys(new_redact_keys=[])
101
+ @request.env['etna.redact_keys'] = (@request.env['etna.redact_keys'] || []).concat(new_redact_keys)
102
+ end
103
+
104
+ def log_request
105
+ censor = Etna::Censor.new(redact_keys)
106
+
107
+ redacted_params = @params.map do |key,value|
108
+ [ key, censor.redact(key, value) ]
109
+ end.to_h
110
+
111
+ log("User #{@user ? @user.email : :unknown} calling #{controller_name}##{@action} with params #{redacted_params}")
112
+ end
113
+
114
+ def controller_name
115
+ self.class.name.sub("Kernel::", "").sub("Controller", "").downcase
116
+ end
117
+
87
118
  def success(msg, content_type='text/plain')
88
119
  @response['Content-Type'] = content_type
89
120
  @response.write(msg)
@@ -3,8 +3,7 @@ require 'fileutils'
3
3
  require 'open3'
4
4
  require 'securerandom'
5
5
  require 'concurrent-ruby'
6
- require 'net/sftp'
7
- require 'net/ssh'
6
+ require 'curb'
8
7
 
9
8
  module Etna
10
9
  # A class that encapsulates opening / reading file system entries that abstracts normal file access in order
@@ -377,55 +376,116 @@ module Etna
377
376
  end
378
377
 
379
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
+
380
392
  def initialize(host:, username:, password: nil, port: 22, **args)
381
393
  @username = username
382
394
  @password = password
383
395
  @host = host
384
396
  @port = port
397
+
398
+ @dir_listings = {}
399
+ end
400
+
401
+ def url(src)
402
+ "sftp://#{@host}/#{src}"
385
403
  end
386
404
 
387
- def ssh
388
- @ssh ||= Net::SSH.start(@host, @username, password: @password)
405
+ def authn
406
+ "#{@username}:#{@password}"
389
407
  end
390
408
 
391
- def sftp
392
- @sftp ||= begin
393
- conn = Net::SFTP::Session.new(ssh)
394
- conn.loop { conn.opening? }
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
395
414
 
396
- conn
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)
397
423
  end
424
+
425
+ raise "#{src} not found" if file.empty?
426
+
427
+ file.first
398
428
  end
399
429
 
400
- def with_readable(src, opts = 'r', &block)
401
- sftp.file.open(src, opts, &block)
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
402
446
  end
403
447
 
404
- def ls(dir)
405
- sftp.dir.entries(dir)
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)
406
454
  end
407
455
 
408
456
  def exist?(src)
409
- begin
410
- sftp.file.open(src)
411
- rescue Net::SFTP::StatusException
412
- return false
413
- end
414
- return true
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
415
474
  end
416
475
 
417
476
  def stat(src)
418
- sftp.file.open(src).stat
477
+ sftp_file_from_path(src)
419
478
  end
420
479
  end
421
480
 
422
481
  class Mock < Filesystem
423
482
  class MockStat
424
- def initialize
483
+ def initialize(io)
484
+ @io = io
425
485
  end
426
486
 
427
487
  def size
428
- 0
488
+ @io.respond_to?(:length) ? @io.length : 0
429
489
  end
430
490
  end
431
491
 
@@ -499,7 +559,7 @@ module Etna
499
559
  end
500
560
 
501
561
  def stat(src)
502
- @files[src].respond_to?(:stat) ? @files[src].stat : MockStat.new
562
+ @files[src].respond_to?(:stat) ? @files[src].stat : MockStat.new(@files[src])
503
563
  end
504
564
  end
505
565
  end
@@ -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,5 +1,6 @@
1
1
  require 'digest'
2
2
  require 'date'
3
+ require_relative "./censor"
3
4
 
4
5
  module Etna
5
6
  class Route
@@ -13,6 +14,7 @@ module Etna
13
14
  @route = route.gsub(/\A(?=[^\/])/, '/')
14
15
  @block = block
15
16
  @match_ext = options[:match_ext]
17
+ @log_redact_keys = options[:log_redact_keys]
16
18
  end
17
19
 
18
20
  def to_hash
@@ -124,6 +126,8 @@ module Etna
124
126
  return [ 403, { 'Content-Type' => 'application/json' }, [ { error: 'You are forbidden from performing this action.' }.to_json ] ]
125
127
  end
126
128
 
129
+ request.env['etna.redact_keys'] = @log_redact_keys
130
+
127
131
  if @action
128
132
  controller, action = @action.split('#')
129
133
  controller_class = Kernel.const_get(
@@ -132,11 +136,6 @@ module Etna
132
136
  logger = request.env['etna.logger']
133
137
  user = request.env['etna.user']
134
138
 
135
- params = request.env['rack.request.params'].map do |key,value|
136
- [ key, redact(key, value) ]
137
- end.to_h
138
-
139
- logger.warn("User #{user ? user.email : :unknown} calling #{controller}##{action} with params #{params}")
140
139
  return controller_class.new(request, action).response
141
140
  elsif @block
142
141
  application = Etna::Application.find(app.class).class
@@ -161,35 +160,6 @@ module Etna
161
160
  @application ||= Etna::Application.instance
162
161
  end
163
162
 
164
- def compact(value)
165
- value = value.to_s
166
- value = value[0..500] + "..." + value[-100..-1] if value.length > 600
167
- value
168
- end
169
-
170
- def redact_keys
171
- @redact_keys ||= application.config(:log_redact_keys).split(",").map do |key|
172
- key.to_sym
173
- end
174
- end
175
-
176
- def redact(key, value)
177
- # From configuration, redact any values for the supplied key values, so they
178
- # don't appear in the logs.
179
- return compact(value) unless application.config(:log_redact_keys)
180
-
181
- if value.is_a?(Hash)
182
- redacted_value = value.map do |value_key, value_value|
183
- [ value_key, redact(value_key, value_value) ]
184
- end.to_h
185
- return redacted_value
186
- elsif redact_keys.include?(key)
187
- return "*"
188
- end
189
-
190
- return compact(value)
191
- end
192
-
193
163
  def authorized?(request)
194
164
  # If there is no @auth requirement, they are ok - this doesn't preclude
195
165
  # them being rejected in the controller response
@@ -0,0 +1,14 @@
1
+ module Etna
2
+ class SynchronizeDb
3
+ def initialize(app)
4
+ @app = app
5
+ end
6
+
7
+ def call(env)
8
+ # Do a coarse checkout of the connection
9
+ Etna::Application.instance.db.synchronize do
10
+ @app.call(env)
11
+ end
12
+ end
13
+ end
14
+ end
data/lib/etna.rb CHANGED
@@ -22,6 +22,8 @@ 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'
26
+ require_relative './etna/synchronize_db'
25
27
 
26
28
  class EtnaApp
27
29
  include Etna::Application
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.39
4
+ version: 0.1.43
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-07-22 00:00:00.000000000 Z
11
+ date: 2021-10-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -95,19 +95,33 @@ dependencies:
95
95
  - !ruby/object:Gem::Version
96
96
  version: '0'
97
97
  - !ruby/object:Gem::Dependency
98
- name: net-sftp
98
+ name: curb
99
99
  requirement: !ruby/object:Gem::Requirement
100
100
  requirements:
101
101
  - - ">="
102
102
  - !ruby/object:Gem::Version
103
- version: 3.0.0
103
+ version: '0'
104
104
  type: :runtime
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
108
  - - ">="
109
109
  - !ruby/object:Gem::Version
110
- version: 3.0.0
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'
111
125
  description: See summary
112
126
  email: Saurabh.Asthana@ucsf.edu
113
127
  executables:
@@ -124,6 +138,7 @@ files:
124
138
  - lib/etna.rb
125
139
  - lib/etna/application.rb
126
140
  - lib/etna/auth.rb
141
+ - lib/etna/censor.rb
127
142
  - lib/etna/client.rb
128
143
  - lib/etna/clients.rb
129
144
  - lib/etna/clients/base_client.rb
@@ -186,6 +201,7 @@ files:
186
201
  - lib/etna/metrics.rb
187
202
  - lib/etna/multipart_serializable_nested_hash.rb
188
203
  - lib/etna/parse_body.rb
204
+ - lib/etna/remote.rb
189
205
  - lib/etna/route.rb
190
206
  - lib/etna/server.rb
191
207
  - lib/etna/sign_service.rb
@@ -193,6 +209,7 @@ files:
193
209
  - lib/etna/spec/auth.rb
194
210
  - lib/etna/spec/vcr.rb
195
211
  - lib/etna/symbolize_params.rb
212
+ - lib/etna/synchronize_db.rb
196
213
  - lib/etna/templates/attribute_actions_template.json
197
214
  - lib/etna/test_auth.rb
198
215
  - lib/etna/user.rb