cnvrg 1.2.7 → 1.3.2
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 +4 -4
- data/cnvrg.gemspec +3 -2
- data/lib/cnvrg/api.rb +2 -2
- data/lib/cnvrg/cli.rb +51 -36
- data/lib/cnvrg/data.rb +14 -3
- data/lib/cnvrg/datafiles.rb +20 -62
- data/lib/cnvrg/dataset.rb +198 -144
- data/lib/cnvrg/downloader/client.rb +53 -0
- data/lib/cnvrg/downloader/clients/azure_client.rb +22 -0
- data/lib/cnvrg/downloader/clients/gcp_client.rb +46 -0
- data/lib/cnvrg/downloader/clients/s3_client.rb +51 -0
- data/lib/cnvrg/files.rb +17 -44
- data/lib/cnvrg/helpers/executer.rb +171 -0
- data/lib/cnvrg/job_cli.rb +33 -0
- data/lib/cnvrg/project.rb +45 -6
- data/lib/cnvrg/storage.rb +128 -0
- data/lib/cnvrg/version.rb +1 -1
- metadata +28 -7
- data/lib/cnvrg/job.rb +0 -40
@@ -0,0 +1,53 @@
|
|
1
|
+
|
2
|
+
module Cnvrg
|
3
|
+
module Downloader
|
4
|
+
OLD_SERVER_VERSION_MESSAGE = "Your server version is not relevant for this cli version please contact support for further help."
|
5
|
+
attr_accessor :bucket, :client
|
6
|
+
class Client
|
7
|
+
def initialize(params)
|
8
|
+
@key = ''
|
9
|
+
@iv = ''
|
10
|
+
@client = ''
|
11
|
+
@bucket = ''
|
12
|
+
end
|
13
|
+
|
14
|
+
def extract_key_iv(sts_path)
|
15
|
+
sts = open(sts_path).read rescue nil
|
16
|
+
raise StandardError.new("Cant open sts") if sts.blank?
|
17
|
+
sts.split("\n")
|
18
|
+
end
|
19
|
+
|
20
|
+
def download(storage_path, local_path)
|
21
|
+
### need to be implemented..
|
22
|
+
end
|
23
|
+
|
24
|
+
def upload(storage_path, local_path)
|
25
|
+
### need to be implemented..
|
26
|
+
end
|
27
|
+
|
28
|
+
def mkdir(path, recursive: false)
|
29
|
+
recursive ? FileUtils.mkdir_p(path) : FileUtils.mkdir(path)
|
30
|
+
end
|
31
|
+
|
32
|
+
def prepare_download(local_path)
|
33
|
+
mkdir(File.dirname(local_path), recursive: true)
|
34
|
+
end
|
35
|
+
|
36
|
+
def decrypt(str)
|
37
|
+
Cnvrg::Helpers.decrypt(@key, @iv, str)
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.factory(params)
|
41
|
+
params = params.as_json
|
42
|
+
case params["storage"]
|
43
|
+
when 's3', 'minio'
|
44
|
+
return Cnvrg::Downloader::Clients::S3Client.new(sts_path: params["path_sts"], access_key: params["sts_a"], secret: params["sts_s"], session_token: params["sts_st"], region: params["region"], bucket: params["bucket"], encryption: params["encryption"], endpoint: params["endpoint"], storage: params["storage"])
|
45
|
+
when 'azure'
|
46
|
+
return Cnvrg::Downloader::Clients::AzureDownloader.new(params)
|
47
|
+
when 'gcp'
|
48
|
+
return Cnvrg::Downloader::Clients::GcpClient.new(project_id: params["project_id"], credentials: params["credentials"], bucket_name: params["bucket_name"], sts: params["sts"])
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Cnvrg
|
2
|
+
module Downloader
|
3
|
+
module Clients
|
4
|
+
class AzureClient < Client
|
5
|
+
def initialize(storage: nil, account_name: nil, access_key: nil, container: nil, sts_path: nil)
|
6
|
+
@key, @iv = extract_key_iv(sts_path)
|
7
|
+
@account_name = Cnvrg::Helpers.decrypt(@key, @iv, account_name)
|
8
|
+
@access_key = Cnvrg::Helpers.decrypt(@key, @iv, access_key)
|
9
|
+
@container = Cnvrg::Helpers.decrypt(@key, @iv, container)
|
10
|
+
end
|
11
|
+
|
12
|
+
def download(storage_path, local_path)
|
13
|
+
|
14
|
+
end
|
15
|
+
|
16
|
+
def upload(storage_path, local_path)
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require "google/cloud/storage"
|
2
|
+
|
3
|
+
module Cnvrg
|
4
|
+
module Downloader
|
5
|
+
module Clients
|
6
|
+
class GcpClient < Client
|
7
|
+
def initialize(project_id: nil, credentials: nil, bucket_name: nil, sts: nil)
|
8
|
+
@key, @iv = extract_key_iv(sts)
|
9
|
+
@project_id = Cnvrg::Helpers.decrypt(@key, @iv, project_id)
|
10
|
+
@credentials_path = Cnvrg::Helpers.decrypt(@key, @iv, credentials)
|
11
|
+
@tempfile = nil
|
12
|
+
@bucket_name = Cnvrg::Helpers.decrypt(@key, @iv, bucket_name)
|
13
|
+
init_gcp_credentials
|
14
|
+
@storage = Google::Cloud::Storage.new(project_id: @project_id, credentials: @credentials)
|
15
|
+
@bucket = @storage.bucket(@bucket_name)
|
16
|
+
@bucket.name
|
17
|
+
rescue => e
|
18
|
+
Cnvrg::Logger.log_error(e)
|
19
|
+
Cnvrg::Logger.log_info("Tried to init gcp client without success.")
|
20
|
+
Cnvrg::CLI.log_message("Cannot init client. please contact support to check your bucket credentials.")
|
21
|
+
exit(1)
|
22
|
+
end
|
23
|
+
|
24
|
+
def init_gcp_credentials
|
25
|
+
t = Tempfile.new
|
26
|
+
f = open(@credentials_path).read
|
27
|
+
t.binmode
|
28
|
+
t.write(f)
|
29
|
+
t.rewind
|
30
|
+
@credentials = t.path
|
31
|
+
@tempfile = t
|
32
|
+
end
|
33
|
+
|
34
|
+
def download(storage_path, local_path)
|
35
|
+
prepare_download(local_path)
|
36
|
+
file = @bucket.file(decrypt(storage_path))
|
37
|
+
file.download local_path
|
38
|
+
end
|
39
|
+
|
40
|
+
def upload(storage_path, local_path)
|
41
|
+
@bucket.create_file(local_path, storage_path)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Cnvrg
|
2
|
+
module Downloader
|
3
|
+
module Clients
|
4
|
+
class S3Client < Client
|
5
|
+
def initialize(sts_path: nil, access_key: nil, secret: nil, session_token: nil, region: nil, bucket: nil, encryption: nil, endpoint: nil, storage: nil)
|
6
|
+
@key, @iv = extract_key_iv(sts_path)
|
7
|
+
@access_key = Cnvrg::Helpers.decrypt(@key, @iv, access_key)
|
8
|
+
@secret = Cnvrg::Helpers.decrypt(@key, @iv, secret)
|
9
|
+
@session_token = Cnvrg::Helpers.decrypt(@key, @iv, session_token)
|
10
|
+
@region = Cnvrg::Helpers.decrypt(@key, @iv, region)
|
11
|
+
@bucket_name = Cnvrg::Helpers.decrypt(@key, @iv, bucket)
|
12
|
+
@endpoint = Cnvrg::Helpers.decrypt(@key, @iv, endpoint)
|
13
|
+
options = {
|
14
|
+
:access_key_id => @access_key,
|
15
|
+
:secret_access_key => @secret,
|
16
|
+
:session_token => @session_token,
|
17
|
+
:region => @region,
|
18
|
+
:http_open_timeout => 60, :retry_limit => 20
|
19
|
+
}
|
20
|
+
if storage == 'minio'
|
21
|
+
options[:endpoint] = @endpoint
|
22
|
+
end
|
23
|
+
|
24
|
+
@client = Aws::S3::Client.new(options)
|
25
|
+
@bucket = Aws::S3::Resource.new(client: @client).bucket(@bucket_name)
|
26
|
+
@upload_options = {:use_accelerate_endpoint => storage == 's3'}
|
27
|
+
if encryption.present?
|
28
|
+
@upload_options[:server_side_encryption] = encryption
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def download(storage_path, local_path)
|
33
|
+
prepare_download(local_path)
|
34
|
+
storage_path = Cnvrg::Helpers.decrypt(@key, @iv, storage_path)
|
35
|
+
File.open(local_path, 'w+') do |file|
|
36
|
+
resp = @client.get_object({bucket: @bucket_name,
|
37
|
+
key: storage_path}, target: file)
|
38
|
+
end
|
39
|
+
resp
|
40
|
+
end
|
41
|
+
|
42
|
+
def upload(storage_path, local_path)
|
43
|
+
### storage path is the path inside s3 (after the bucket)
|
44
|
+
# local path is fullpath for the file /home/ubuntu/user.../hazilim.py
|
45
|
+
o = @bucket.object(storage_path)
|
46
|
+
o.upload_file(local_path, @upload_options)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
data/lib/cnvrg/files.rb
CHANGED
@@ -18,6 +18,10 @@ module Cnvrg
|
|
18
18
|
@base_resource = "users/#{owner}/projects/#{project_slug}/"
|
19
19
|
@project_home = project_home.presence || Cnvrg::CLI.get_project_home
|
20
20
|
@project = project
|
21
|
+
@client = nil
|
22
|
+
if @project.present?
|
23
|
+
@client = @project.get_storage_client
|
24
|
+
end
|
21
25
|
@progressbar = progressbar
|
22
26
|
@custom_progess = false
|
23
27
|
@cli = cli
|
@@ -84,16 +88,10 @@ module Cnvrg
|
|
84
88
|
# resolve bucket
|
85
89
|
res = resp['result']
|
86
90
|
files = res['files']
|
87
|
-
props = Cnvrg::Helpers.get_s3_props(res)
|
88
|
-
client = props[:client]
|
89
|
-
bucket = props[:bucket]
|
90
|
-
upload_options = props[:upload_options]
|
91
|
-
s3_bucket = Aws::S3::Resource.new(client: client).bucket(bucket)
|
92
|
-
|
93
91
|
#upload files
|
94
92
|
# files.keys.map do |file|
|
95
93
|
Parallel.map(files.keys, self.get_upload_options) do |file|
|
96
|
-
resp = Cnvrg::Helpers.try_until_success{self.upload_single_file(files[file].merge(files_list[file])
|
94
|
+
resp = Cnvrg::Helpers.try_until_success{self.upload_single_file(files[file].merge(files_list[file]))}
|
97
95
|
raise SignalException.new("Cant upload #{file}") unless resp
|
98
96
|
progress.progress += 1 if progress.present?
|
99
97
|
end
|
@@ -134,16 +132,10 @@ module Cnvrg
|
|
134
132
|
end
|
135
133
|
end
|
136
134
|
|
137
|
-
def upload_single_file(file
|
135
|
+
def upload_single_file(file)
|
138
136
|
path = file['path']
|
139
137
|
absolute_path = file[:absolute_path]
|
140
|
-
|
141
|
-
resp = bucket.object(path).
|
142
|
-
upload_file(absolute_path, options)
|
143
|
-
unless resp
|
144
|
-
raise SignalException.new("Cant upload #{absolute_path}")
|
145
|
-
end
|
146
|
-
resp
|
138
|
+
@client.upload(path, absolute_path)
|
147
139
|
end
|
148
140
|
|
149
141
|
def parse_file(file)
|
@@ -192,15 +184,9 @@ module Cnvrg
|
|
192
184
|
upload_resp = Cnvrg::API.request("/users/#{@owner}/" + "upload_cli_log", 'POST_FILE', {absolute_path: absolute_path, relative_path: relative_path,
|
193
185
|
file_name: file_name, log_date: log_date,
|
194
186
|
file_size: file_size, file_content_type: content_type})
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
end
|
199
|
-
if s3_res
|
200
|
-
return true
|
201
|
-
end
|
202
|
-
return false
|
203
|
-
|
187
|
+
path, client = upload_resp["path"], upload_resp["client"]
|
188
|
+
@client = Cnvrg::Downloader::Client.factory(client)
|
189
|
+
@client.upload(path, absolute_path)
|
204
190
|
end
|
205
191
|
|
206
192
|
def upload_exec_file(absolute_path, image_name, commit_id)
|
@@ -711,7 +697,7 @@ module Cnvrg
|
|
711
697
|
|
712
698
|
def download_multpile_files_s3(files, project_home, postfix: '', progress: nil)
|
713
699
|
begin
|
714
|
-
props =
|
700
|
+
props = {}
|
715
701
|
client = props[:client]
|
716
702
|
iv = props[:iv]
|
717
703
|
key = props[:key]
|
@@ -739,20 +725,11 @@ module Cnvrg
|
|
739
725
|
if not File.exists?(project_home+"/"+File.dirname(file_path))
|
740
726
|
FileUtils.makedirs(project_home+"/"+File.dirname(file_path))
|
741
727
|
end
|
742
|
-
|
743
|
-
|
744
|
-
|
745
|
-
Cnvrg::Helpers.try_until_success(tries: 10) {
|
746
|
-
File.open(project_home+"/"+file_path, 'w+') do |file|
|
747
|
-
resp = client.get_object({bucket:bucket,
|
748
|
-
key:file_key}, target: file)
|
749
|
-
end
|
750
|
-
}
|
728
|
+
local_path = project_home+"/"+file_path
|
729
|
+
storage_path = f["path"]
|
730
|
+
@client.download(storage_path, local_path)
|
751
731
|
progress.progress += 1 if progress.present?
|
752
732
|
download_succ_count += 1
|
753
|
-
|
754
|
-
|
755
|
-
|
756
733
|
rescue => e
|
757
734
|
return Cnvrg::Result.new(false,"Could not create file: #{file_path}", e.message, e.backtrace)
|
758
735
|
raise Parallel::Kill
|
@@ -913,12 +890,8 @@ module Cnvrg
|
|
913
890
|
end
|
914
891
|
|
915
892
|
def download_file(file_path: '', key: '', iv: '', bucket: '', path: '', client: nil)
|
916
|
-
|
917
|
-
|
918
|
-
File.open(@project_home+"/"+file_path, 'w+') do |file|
|
919
|
-
resp = client.get_object({bucket:bucket,
|
920
|
-
key:file_key}, target: file)
|
921
|
-
end
|
893
|
+
local_path = @project_home+"/"+file_path
|
894
|
+
@client.download(path, local_path)
|
922
895
|
end
|
923
896
|
|
924
897
|
def delete(file)
|
@@ -936,7 +909,7 @@ module Cnvrg
|
|
936
909
|
def handle_compare_idx(compared, resolver: {})
|
937
910
|
begin
|
938
911
|
all_files = compared.values.flatten.uniq
|
939
|
-
props =
|
912
|
+
props = {}
|
940
913
|
files = resolver['keys'].map{|f| [f['name'], f]}.to_h
|
941
914
|
client = props[:client]
|
942
915
|
iv = props[:iv]
|
@@ -0,0 +1,171 @@
|
|
1
|
+
class Cnvrg::Helpers::Executer
|
2
|
+
def initialize(project: nil, job_type: nil, job_id: nil)
|
3
|
+
@project = project
|
4
|
+
@job_type = job_type
|
5
|
+
@job_id = job_id
|
6
|
+
if job_id.blank?
|
7
|
+
Cnvrg::CLI.log_message("Cant find job, exiting.", 'red')
|
8
|
+
exit(1)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def fetch_commands(block: nil, key: nil)
|
13
|
+
resp = Cnvrg::API.request("#{base_url}/commands", "GET", {block: block})
|
14
|
+
commands = decrypt_commands(resp["commands"], resp["key"], resp["iv"])
|
15
|
+
commands.map{|k| k.with_indifferent_access}
|
16
|
+
end
|
17
|
+
|
18
|
+
def decrypt_commands(text,key,iv)
|
19
|
+
text, key, iv = [text,key,iv].map{|x| x.unpack('m')[0]}
|
20
|
+
decipher = OpenSSL::Cipher::AES256.new :CBC
|
21
|
+
decipher.decrypt
|
22
|
+
decipher.key = key
|
23
|
+
decipher.iv = iv
|
24
|
+
commands = decipher.update(text) + decipher.final
|
25
|
+
JSON.parse(commands)
|
26
|
+
end
|
27
|
+
|
28
|
+
def execute(cmd)
|
29
|
+
## execute the command for running
|
30
|
+
# cmd will have to following fields
|
31
|
+
#
|
32
|
+
# :command => the command to execute
|
33
|
+
# :type => the command type, 'notify' or nil
|
34
|
+
# :timeout => the timeout for the command in seconds (default is 60 hours)
|
35
|
+
# :retries => integer, default 1
|
36
|
+
#
|
37
|
+
# when type == 'file_exists'
|
38
|
+
# 'file' => string => file to check (fullpath)
|
39
|
+
# 'exists_commands' => list of commands in case file exists
|
40
|
+
# 'non_exists_commands' => list of commands in case file doesnt exists
|
41
|
+
# when type == 'notify'
|
42
|
+
# :before_execute_log => log to be logged before execution
|
43
|
+
# :logs => boolean => add the execution logs to the job logs
|
44
|
+
# :title => command title, can replace the on_error, on_success fields
|
45
|
+
# :on_error_log => log to be logged on exit_code != 0
|
46
|
+
# :on_success_log => log to be logged on exit_code == 0
|
47
|
+
#
|
48
|
+
retries = cmd[:retries] || 1
|
49
|
+
resp = []
|
50
|
+
retries.times.each do
|
51
|
+
resp = execute_helper(cmd)
|
52
|
+
exit_status, _, _, _, _ = resp
|
53
|
+
return resp if exit_status == 0
|
54
|
+
end
|
55
|
+
return resp
|
56
|
+
end
|
57
|
+
|
58
|
+
def execute_cmds(cmds)
|
59
|
+
cmds.each do |command|
|
60
|
+
puts "===================="
|
61
|
+
puts "Execute #{command[:command]}"
|
62
|
+
execute(command)
|
63
|
+
puts "===================="
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
|
68
|
+
private
|
69
|
+
def execute_helper(cmd)
|
70
|
+
case cmd[:type]
|
71
|
+
when 'notify'
|
72
|
+
return run_and_notify(cmd)
|
73
|
+
when 'file_exists'
|
74
|
+
if File.exists? cmd[:file]
|
75
|
+
return execute_cmds(cmd[:exists_commands]) if cmd[:exists_commands].present?
|
76
|
+
else
|
77
|
+
return execute_cmds(cmd[:non_exists_commands]) if cmd[:non_exists_commands].present?
|
78
|
+
end
|
79
|
+
when 'create_file'
|
80
|
+
return create_file(cmd)
|
81
|
+
else
|
82
|
+
return regular_command(cmd)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def run_and_notify(cmd)
|
87
|
+
with_logs = cmd[:logs]
|
88
|
+
cmd = init_cmd_logs(cmd)
|
89
|
+
job_log(cmd[:before_execute_log]) if cmd[:before_execute_log].present?
|
90
|
+
exit_status, output, errors, start_time, end_time = regular_command(cmd)
|
91
|
+
logs = []
|
92
|
+
if exit_status == 0
|
93
|
+
logs = output.map{|log| log[:logs]} if with_logs
|
94
|
+
job_log(logs + cmd[:on_success_log])
|
95
|
+
else
|
96
|
+
logs = output + errors
|
97
|
+
logs = logs.sort_by{|x| x[:timestamp]}.map{|x| x[:logs]} if with_logs
|
98
|
+
job_log(logs + cmd[:on_error_log], level: 'error')
|
99
|
+
end
|
100
|
+
return [exit_status, output, errors, start_time, end_time]
|
101
|
+
end
|
102
|
+
|
103
|
+
def merge_log_block(logs)
|
104
|
+
logs.group_by {|log| log[:timestamp].to_s}
|
105
|
+
.map {|ts, logz| {timestamp: ts, logs: logz.map {|l| l[:log]}.join("\n")}}
|
106
|
+
end
|
107
|
+
|
108
|
+
|
109
|
+
def regular_command(cmd = {})
|
110
|
+
errors = []
|
111
|
+
output = []
|
112
|
+
start_time = Time.now
|
113
|
+
timeout = cmd[:timeout] || 60*60*60
|
114
|
+
t = Thread.new do
|
115
|
+
PTY.spawn(cmd[:command]) do |stdout, stdin, pid, stderr|
|
116
|
+
begin
|
117
|
+
stdout.each do |line|
|
118
|
+
puts line
|
119
|
+
output << {log: line.strip, timestamp: Time.now}
|
120
|
+
end
|
121
|
+
if stderr.present?
|
122
|
+
stderr.each do |line|
|
123
|
+
errors << {log: line.strip, timestamp: Time.now}
|
124
|
+
end
|
125
|
+
end
|
126
|
+
rescue => e
|
127
|
+
errors << {log: e.message, timestamp: Time.now}
|
128
|
+
end
|
129
|
+
::Process.wait pid
|
130
|
+
end
|
131
|
+
end
|
132
|
+
while t.status
|
133
|
+
if Time.now - start_time > timeout
|
134
|
+
puts "Kill thread because of timeout..."
|
135
|
+
errors << {log: "Timeout", timestamp: Time.now}
|
136
|
+
Thread.kill(t)
|
137
|
+
end
|
138
|
+
sleep 1
|
139
|
+
end
|
140
|
+
exit_status = $?.exitstatus
|
141
|
+
end_time = Time.now
|
142
|
+
[exit_status, merge_log_block(output), merge_log_block(errors), start_time, end_time]
|
143
|
+
end
|
144
|
+
|
145
|
+
def create_file(cmd)
|
146
|
+
cmd = init_cmd_logs(cmd)
|
147
|
+
File.open(cmd[:path], "w+"){|f| f.write(cmd[:content])}
|
148
|
+
end
|
149
|
+
|
150
|
+
def init_cmd_logs(cmd)
|
151
|
+
if cmd[:title]
|
152
|
+
cmd[:before_execute_log] ||= ["Running #{cmd[:title]}"]
|
153
|
+
cmd[:on_error_log] ||= ["#{cmd[:title]} was failed during running"]
|
154
|
+
cmd[:on_success_log] ||= ["#{cmd[:title]} executed successfully"]
|
155
|
+
end
|
156
|
+
cmd[:on_success_log] ||= []
|
157
|
+
cmd[:on_error_log] ||= []
|
158
|
+
[:before_execute_log, :on_success_log, :on_error_log].each{|x| cmd[x] = [cmd[x]].flatten}
|
159
|
+
cmd
|
160
|
+
end
|
161
|
+
|
162
|
+
def base_url
|
163
|
+
"users/#{@project.owner}/projects/#{@project.slug}/jobs/#{@job_type.underscore}/#{@job_id}"
|
164
|
+
end
|
165
|
+
|
166
|
+
|
167
|
+
def job_log(logs, level: 'info', step: nil)
|
168
|
+
@project.job_log(logs, level: level, step: step, job_type: @job_type, job_id: @job_id)
|
169
|
+
end
|
170
|
+
|
171
|
+
end
|