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.
@@ -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
@@ -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]), s3_bucket, options: upload_options)}
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, bucket, options: {})
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
- if Cnvrg::CLI.is_response_success(upload_resp, false)
196
- s3_res = upload_large_files_s3(upload_resp, absolute_path)
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 = Cnvrg::Helpers.get_s3_props(files)
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
- file_key = Cnvrg::Helpers.decrypt(key,iv, f["path"])
744
- resp = false
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
- file_key = Cnvrg::Helpers.decrypt(key,iv, path)
917
- resp = false
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 = Cnvrg::Helpers.get_s3_props(resolver)
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