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 +4 -4
- data/etna.completion +52 -3
- data/lib/commands.rb +19 -0
- data/lib/etna/application.rb +18 -13
- data/lib/etna/censor.rb +36 -0
- data/lib/etna/clients/magma/models.rb +11 -2
- data/lib/etna/clients/metis/client.rb +77 -30
- data/lib/etna/clients/metis/models.rb +86 -2
- data/lib/etna/clients/metis/workflows/ingest_metis_data_workflow.rb +10 -6
- data/lib/etna/clients/metis/workflows/metis_upload_workflow.rb +0 -2
- data/lib/etna/controller.rb +32 -1
- data/lib/etna/filesystem.rb +83 -23
- data/lib/etna/remote.rb +38 -0
- data/lib/etna/route.rb +4 -34
- data/lib/etna/synchronize_db.rb +14 -0
- data/lib/etna.rb +2 -0
- metadata +22 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2e30f0d83fdf8f5781f723080ddd042d371f3131a415c85ec441b56178cef27c
|
4
|
+
data.tar.gz: a543a3464ca34e0d6daedddbf702cf067f7dfdcbdc972e4e9a9a626118d5fbbd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
|
data/lib/etna/application.rb
CHANGED
@@ -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
|
-
|
205
|
+
|
206
|
+
write_job_metrics("run_command.#{cmd.class.name}")
|
202
207
|
end
|
203
208
|
end
|
204
209
|
end
|
data/lib/etna/censor.rb
ADDED
@@ -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
|
-
|
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
|
214
|
-
|
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
|
-
|
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
|
-
|
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:
|
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
|
-
|
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
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
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
|
-
|
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 |
|
25
|
-
metis_filesystem.do_streaming_upload(
|
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
|
data/lib/etna/controller.rb
CHANGED
@@ -42,7 +42,10 @@ module Etna
|
|
42
42
|
|
43
43
|
[501, {}, ['This controller is not implemented.']]
|
44
44
|
rescue Exception => e
|
45
|
-
|
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)
|
data/lib/etna/filesystem.rb
CHANGED
@@ -3,8 +3,7 @@ require 'fileutils'
|
|
3
3
|
require 'open3'
|
4
4
|
require 'securerandom'
|
5
5
|
require 'concurrent-ruby'
|
6
|
-
require '
|
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
|
388
|
-
@
|
405
|
+
def authn
|
406
|
+
"#{@username}:#{@password}"
|
389
407
|
end
|
390
408
|
|
391
|
-
def
|
392
|
-
|
393
|
-
|
394
|
-
|
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
|
-
|
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
|
401
|
-
|
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
|
405
|
-
|
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
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
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
|
-
|
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
|
data/lib/etna/remote.rb
ADDED
@@ -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
|
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.
|
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-
|
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:
|
98
|
+
name: curb
|
99
99
|
requirement: !ruby/object:Gem::Requirement
|
100
100
|
requirements:
|
101
101
|
- - ">="
|
102
102
|
- !ruby/object:Gem::Version
|
103
|
-
version:
|
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:
|
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
|