mobilize-ssh 1.1.10 → 1.2

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -84,7 +84,7 @@ over to the nodes. They will be deleted afterwards, unless the job
84
84
  fails in mid-copy. By default this is tmp/file/.
85
85
  * nodes, identified by aliases, such as `test_node`. This alias is what you should
86
86
  pass into the "node" param over in the ssh.run task.
87
- * default_node is where commands will be executed if no node is specified.
87
+ * if no node is specified, commands will default to the first node listed.
88
88
 
89
89
  Each node has:
90
90
  * a host;
@@ -110,7 +110,6 @@ Sample ssh.yml:
110
110
  ---
111
111
  development:
112
112
  tmp_file_dir: tmp/file/
113
- default_node: dev_node
114
113
  nodes:
115
114
  dev_node:
116
115
  sudoers:
@@ -128,7 +127,6 @@ development:
128
127
  user: gateway_user
129
128
  test:
130
129
  tmp_file_dir: tmp/file/
131
- default_node: test_node
132
130
  nodes:
133
131
  test_node:
134
132
  sudoers:
@@ -146,7 +144,6 @@ test:
146
144
  user: gateway_user
147
145
  production:
148
146
  tmp_file_dir: tmp/file/
149
- default_node: prod_node
150
147
  nodes:
151
148
  prod_node:
152
149
  sudoers:
@@ -172,13 +169,15 @@ Start
172
169
  ### Create Job
173
170
 
174
171
  * For mobilize-ssh, the following task is available:
175
- * ssh.run `node: <node_alias>, cmd: <command>, user: user, sources:[*<gsheet_full_paths>]`, which reads
176
- all gsheets, copies them to a temporary folder on the selected node, and
177
- runs the command inside that folder.
172
+ * ssh.run `node: <node_alias>, cmd: <command>, user: user, sources:[*<source_paths>]`, which reads sources, copies them to a temporary folder on the selected node, and runs the command inside that folder.
178
173
  * user, sources, and node are optional; cmd is required.
179
- * specifying user will cause the command to be prefixed with sudo su <user> -c.
180
- * not specifying node will cause the command to be run on the default_node
181
- * The test uses `ssh.run node:"test_node", cmd:"ruby code.rb", user: "root", sources:["Runner_mobilize(test)/code.rb","Runner_mobilize(test)/code.sh"]`
174
+ * specifying user will cause the command to be prefixed with sudo su <user> -c.
175
+ * non-google sources will also be read as the specified user.
176
+ * not specifying node will cause the command to be run on the default node.
177
+ * ssh sources can be specified with syntax
178
+ `ssh://<node><file_full_path>`. If node is omitted, default node will be used.
179
+ * `<node><file_full_path>` and `<file_full_path>` can be used in the context of ssh.run, but if the path has only 1 slash, or none, it will try to find a google sheet or file instead.
180
+ * The test uses `ssh.run node:"test_node", cmd:"ruby code.rb", user: "root", sources:["code.rb","code.sh"]`
182
181
 
183
182
  <a name='section_Start_Run_Test'></a>
184
183
  ### Run Test
@@ -1,14 +1,35 @@
1
1
  class Net::SSH::Connection::Session
2
+ #from http://stackoverflow.com/questions/3386233/how-to-get-exit-status-with-rubys-netssh-library
2
3
  def run(command)
3
- stdout,stderr = ["",""]
4
- self.exec!(command) do |ch, stream, data|
5
- if stream == :stderr
6
- stderr += data
7
- else
8
- stdout += data
4
+ ssh = self
5
+ stdout_data = ""
6
+ stderr_data = ""
7
+ exit_code = nil
8
+ exit_signal = nil
9
+ ssh.open_channel do |channel|
10
+ channel.exec(command) do |chan, success|
11
+ unless success
12
+ abort "FAILED: couldn't execute command (ssh.channel.exec)"
13
+ end
14
+ channel.on_data do |ch,data|
15
+ stdout_data+=data
16
+ end
17
+
18
+ channel.on_extended_data do |ch,type,data|
19
+ stderr_data+=data
20
+ end
21
+
22
+ channel.on_request("exit-status") do |ch,data|
23
+ exit_code = data.read_long
24
+ end
25
+
26
+ channel.on_request("exit-signal") do |ch, data|
27
+ exit_signal = data.read_long
28
+ end
9
29
  end
10
30
  end
11
- raise stderr if stderr.length>0
12
- return stdout
31
+ ssh.loop
32
+ response = {'stdout'=>stdout_data, 'stderr'=>stderr_data, 'exit_code'=>exit_code, 'exit_signal'=>exit_signal}
33
+ response
13
34
  end
14
35
  end
@@ -1,18 +1,40 @@
1
1
  class Net::SSH::Gateway
2
2
  def self.run(gname,guser,name,user,command,gopts={},opts={})
3
- gateway = self.new(gname,guser,gopts)
3
+ gate = self
4
+ gateway = gate.new(gname,guser,gopts)
5
+ response = nil
4
6
  gateway.ssh(name,user,opts) do |ssh|
5
- stderr,stdout = ["",""]
6
- ssh.exec!(command) do |ch, stream, data|
7
- if stream == :stderr
8
- stderr += data
9
- else
10
- stdout += data
7
+ #from http://stackoverflow.com/questions/3386233/how-to-get-exit-status-with-rubys-netssh-library
8
+ stdout_data = ""
9
+ stderr_data = ""
10
+ exit_code = nil
11
+ exit_signal = nil
12
+ ssh.open_channel do |channel|
13
+ channel.exec(command) do |chan, success|
14
+ unless success
15
+ abort "FAILED: couldn't execute command (ssh.channel.exec)"
16
+ end
17
+ channel.on_data do |ch,data|
18
+ stdout_data+=data
19
+ end
20
+
21
+ channel.on_extended_data do |ch,type,data|
22
+ stderr_data+=data
23
+ end
24
+
25
+ channel.on_request("exit-status") do |ch,data|
26
+ exit_code = data.read_long
27
+ end
28
+
29
+ channel.on_request("exit-signal") do |ch, data|
30
+ exit_signal = data.read_long
31
+ end
11
32
  end
12
33
  end
13
- raise stderr if stderr.length>0
14
- return stdout
34
+ ssh.loop
35
+ response = {'stdout'=>stdout_data, 'stderr'=>stderr_data, 'exit_code'=>exit_code, 'exit_signal'=>exit_signal}
15
36
  end
37
+ response
16
38
  end
17
39
  def self.sync(gname,guser,name,user,from_path,to_path,gopts={},opts={})
18
40
  gateway = self.new(gname,guser,gopts)
@@ -24,8 +24,12 @@ module Mobilize
24
24
  Ssh.config['nodes'][node]['su_all_users']
25
25
  end
26
26
 
27
+ def Ssh.nodes
28
+ Ssh.config['nodes'].keys
29
+ end
30
+
27
31
  def Ssh.default_node
28
- Ssh.config['default_node']
32
+ Ssh.nodes.first
29
33
  end
30
34
 
31
35
  #determine if current machine is on host domain, needs gateway if one is provided and it is not
@@ -53,6 +57,43 @@ module Mobilize
53
57
  return true
54
58
  end
55
59
 
60
+ # converts a source path or target path to a dst in the context of handler and stage
61
+ def Ssh.path_to_dst(path,stage_path)
62
+ has_handler = true if path.index("://")
63
+ red_path = path.split("://").last
64
+ #is user has a handler, their first path node is a node name,
65
+ #or there are more than 2 path nodes, try to find Ssh file
66
+ if has_handler or Ssh.nodes.include?(red_path.split("/").first) or red_path.split("/").length > 2
67
+ user_name = Ssh.user_name_by_stage_path(stage_path)
68
+ ssh_url = Ssh.url_by_path(red_path,user_name)
69
+ return Dataset.find_or_create_by_url(ssh_url)
70
+ end
71
+ #otherwise, use Gsheet
72
+ return Gsheet.path_to_dst(red_path,stage_path)
73
+ end
74
+
75
+ def Ssh.url_by_path(path,user_name)
76
+ node = path.split("/").first.to_s
77
+ if Ssh.nodes.include?(node)
78
+ #cut node out of path
79
+ path = "/" + path.split("/")[1..-1].join("/")
80
+ else
81
+ node = Ssh.default_node
82
+ path = path.starts_with?("/") ? path : "/#{path}"
83
+ end
84
+ url = "ssh://#{node}#{path}"
85
+ begin
86
+ response = Ssh.run(node, "head -1 #{path}", user_name)
87
+ if response['exit_code'] != 0
88
+ raise "Unable to find #{url} with error: #{response['stderr']}"
89
+ else
90
+ return "ssh://#{node}#{path}"
91
+ end
92
+ rescue => exc
93
+ raise Exception, "Unable to find #{url} with error: #{exc.to_s}", exc.backtrace
94
+ end
95
+ end
96
+
56
97
  def Ssh.scp(node,from_path,to_path)
57
98
  name,key,port,user = Ssh.host(node).ie{|h| ['name','key','port','user'].map{|k| h[k]}}
58
99
  key_path = "#{Base.root}/#{key}"
@@ -119,20 +160,29 @@ module Mobilize
119
160
  key_path = "#{Base.root}/#{key}"
120
161
  Ssh.set_key_permissions(key_path)
121
162
  opts = {:port=>(port || 22),:keys=>key_path}
122
- if Ssh.needs_gateway?(node)
123
- gname,gkey,gport,guser = Ssh.gateway(node).ie{|h| ['name','key','port','user'].map{|k| h[k]}}
124
- gkey_path = "#{Base.root}/#{gkey}"
125
- gopts = {:port=>(gport || 22),:keys=>gkey_path}
126
- Net::SSH::Gateway.run(gname,guser,name,user,cmd,gopts,opts)
127
- else
128
- Net::SSH.start(name,user,opts) do |ssh|
129
- ssh.run(cmd)
130
- end
131
- end
163
+ response = if Ssh.needs_gateway?(node)
164
+ gname,gkey,gport,guser = Ssh.gateway(node).ie{|h| ['name','key','port','user'].map{|k| h[k]}}
165
+ gkey_path = "#{Base.root}/#{gkey}"
166
+ gopts = {:port=>(gport || 22),:keys=>gkey_path}
167
+ Net::SSH::Gateway.run(gname,guser,name,user,cmd,gopts,opts)
168
+ else
169
+ Net::SSH.start(name,user,opts) do |ssh|
170
+ ssh.run(cmd)
171
+ end
172
+ end
173
+ response
132
174
  end
133
175
 
134
- def Ssh.read(node,path)
135
- Ssh.fire!(node,"cat #{path}")
176
+ def Ssh.read_by_dataset_path(dst_path,user_name,*args)
177
+ #expects node as first part of path
178
+ node,path = dst_path.split("/").ie{|pa| [pa.first,pa[1..-1].join("/")]}
179
+ #slash in front of path
180
+ response = Ssh.run(node,"cat /#{path}",user_name)
181
+ if response['exit_code'] == 0
182
+ return response['stdout']
183
+ else
184
+ raise "Unable to read ssh://#{dst_path} with error: #{response['stderr']}"
185
+ end
136
186
  end
137
187
 
138
188
  def Ssh.write(node,fdata,to_path,binary=false)
@@ -154,37 +204,59 @@ module Mobilize
154
204
  return tmp_file_path
155
205
  end
156
206
 
157
- def Ssh.run_by_stage_path(stage_path)
207
+ def Ssh.user_name_by_stage_path(stage_path,node=nil)
158
208
  s = Stage.where(:path=>stage_path).first
159
209
  u = s.job.runner.user
210
+ user_name = s.params['user']
211
+ node = s.params['node']
212
+ node = Ssh.default_node unless Ssh.nodes.include?(node)
213
+ if user_name and !Ssh.sudoers(node).include?(u.name)
214
+ raise "#{u.name} does not have su permissions for this node"
215
+ elsif user_name.nil? and Ssh.su_all_users(node)
216
+ user_name = u.name
217
+ end
218
+ return user_name
219
+ end
220
+
221
+ def Ssh.file_hash_by_stage_path(stage_path)
222
+ file_hash = {}
223
+ s = Stage.where(:path=>stage_path).first
224
+ u = s.job.runner.user
225
+ user_name = Ssh.user_name_by_stage_path(stage_path)
226
+ s.sources.each do |sdst|
227
+ split_path = sdst.path.split("/")
228
+ #if path is to stage output, name with stage name
229
+ file_name = if split_path.last == "out" and
230
+ (1..5).to_a.map{|n| "stage#{n.to_s}"}.include?(split_path[-2].to_s)
231
+ "#{split_path[-2]}.out"
232
+ else
233
+ split_path.last
234
+ end
235
+ if ["gsheet","gfile"].include?(sdst.handler)
236
+ #google drive sources are always read as the user
237
+ file_hash[file_name] = sdst.read(u.name)
238
+ else
239
+ #other sources should be read by su-user
240
+ file_hash[file_name] = sdst.read(user_name)
241
+ end
242
+ end
243
+ return file_hash
244
+ end
245
+
246
+ def Ssh.run_by_stage_path(stage_path)
247
+ s = Stage.where(:path=>stage_path).first
160
248
  params = s.params
161
249
  node, command = [params['node'],params['cmd']]
162
250
  node ||= Ssh.default_node
163
- gdrive_slot = Gdrive.slot_worker_by_path(s.path)
164
- return false unless gdrive_slot
165
- file_hash = {}
166
- s.source_dsts(gdrive_slot).each do |sdst|
167
- split_path = sdst.path.split("/")
168
- #if path is to stage output, name with stage name
169
- file_name = if split_path.last == "out" and
170
- (1..5).to_a.map{|n| "stage#{n.to_s}"}.include?(split_path[-2].to_s)
171
- "#{split_path[-2]}.out"
172
- else
173
- split_path.last
174
- end
175
- file_hash[file_name] = sdst.read(u.name)
176
- end
177
- Gdrive.unslot_worker_by_path(s.path)
178
- user = s.params['user']
179
- if user and !Ssh.sudoers(node).include?(u.name)
180
- raise "#{u.name} does not have su permissions for this node"
181
- elsif user.nil? and Ssh.su_all_users(node)
182
- user = u.name
183
- end
184
- out_tsv = Ssh.run(node,command,user,file_hash)
251
+ user_name = Ssh.user_name_by_stage_path(stage_path)
252
+ file_hash = Ssh.file_hash_by_stage_path(stage_path)
253
+ result = Ssh.run(node,command,user_name,file_hash)
185
254
  #use Gridfs to cache result
186
- out_url = "gridfs://#{s.path}/out"
187
- Dataset.write_by_url(out_url,out_tsv,Gdrive.owner_name)
255
+ response = {}
256
+ response['out_url'] = Dataset.write_by_url("gridfs://#{s.path}/out",result['stdout'].to_s,Gdrive.owner_name)
257
+ response['err_url'] = Dataset.write_by_url("gridfs://#{s.path}/err",result['stderr'].to_s,Gdrive.owner_name) if result['stderr'].to_s.length>0
258
+ response['signal'] = result['exit_code']
259
+ response
188
260
  end
189
261
  end
190
262
  end
@@ -1,5 +1,5 @@
1
1
  module Mobilize
2
2
  module Ssh
3
- VERSION = "1.1.10"
3
+ VERSION = "1.2"
4
4
  end
5
5
  end
data/lib/samples/ssh.yml CHANGED
@@ -1,7 +1,6 @@
1
1
  ---
2
2
  development:
3
3
  tmp_file_dir: tmp/file/
4
- default_node: dev_node
5
4
  nodes:
6
5
  dev_node:
7
6
  sudoers:
@@ -19,7 +18,6 @@ development:
19
18
  user: gateway_user
20
19
  test:
21
20
  tmp_file_dir: tmp/file/
22
- default_node: test_node
23
21
  nodes:
24
22
  test_node:
25
23
  sudoers:
@@ -37,7 +35,6 @@ test:
37
35
  user: gateway_user
38
36
  production:
39
37
  tmp_file_dir: tmp/file/
40
- default_node: prod_node
41
38
  nodes:
42
39
  prod_node:
43
40
  sudoers:
data/mobilize-ssh.gemspec CHANGED
@@ -7,7 +7,7 @@ Gem::Specification.new do |gem|
7
7
  gem.name = "mobilize-ssh"
8
8
  gem.version = Mobilize::Ssh::VERSION
9
9
  gem.authors = ["Cassio Paes-Leme"]
10
- gem.email = ["cpaesleme@ngmoco.com"]
10
+ gem.email = ["cpaesleme@dena.com"]
11
11
  gem.description = %q{mobilize-ssh allows you to automate ssh commands and files across hosts}
12
12
  gem.summary = %q{extend mobilize-base with the ability to run files across hosts}
13
13
  gem.homepage = "http://github.com/dena/mobilize-ssh"
@@ -16,7 +16,7 @@ Gem::Specification.new do |gem|
16
16
  gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
17
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
18
  gem.require_paths = ["lib"]
19
- gem.add_runtime_dependency "mobilize-base","1.1.10"
19
+ gem.add_runtime_dependency "mobilize-base","1.2"
20
20
  gem.add_runtime_dependency "net-ssh"
21
21
  gem.add_runtime_dependency "net-scp"
22
22
  gem.add_runtime_dependency "net-ssh-gateway"
data/test/code2.sh ADDED
@@ -0,0 +1 @@
1
+ tail syslog
@@ -20,6 +20,7 @@ describe "Mobilize" do
20
20
 
21
21
  rb_code_sheet = Mobilize::Gsheet.find_by_path("#{r.path.split("/")[0..-2].join("/")}/code.rb",gdrive_slot)
22
22
  sh_code_sheet = Mobilize::Gsheet.find_by_path("#{r.path.split("/")[0..-2].join("/")}/code.sh",gdrive_slot)
23
+ sh_code_sheet2 = Mobilize::Gsheet.find_by_path("#{r.path.split("/")[0..-2].join("/")}/code2.sh",gdrive_slot)
23
24
  [rb_code_sheet,sh_code_sheet].each {|s| s.delete if s}
24
25
 
25
26
  puts "add test code"
@@ -31,6 +32,10 @@ describe "Mobilize" do
31
32
  sh_code_tsv = File.open("#{Mobilize::Base.root}/test/code.sh").read
32
33
  sh_code_sheet.write(sh_code_tsv,Mobilize::Gdrive.owner_name)
33
34
 
35
+ sh_code_sheet2 = Mobilize::Gsheet.find_or_create_by_path("#{r.path.split("/")[0..-2].join("/")}/code2.sh",gdrive_slot)
36
+ sh_code_tsv2 = File.open("#{Mobilize::Base.root}/test/code2.sh").read
37
+ sh_code_sheet2.write(sh_code_tsv2,Mobilize::Gdrive.owner_name)
38
+
34
39
  jobs_sheet = r.gsheet(gdrive_slot)
35
40
 
36
41
  #delete target sheets if they exist
@@ -43,9 +48,9 @@ describe "Mobilize" do
43
48
  ssh_job_rows.map{|j| r.jobs(j['name'])}.each{|j| j.delete if j}
44
49
  jobs_sheet.add_or_update_rows(ssh_job_rows)
45
50
 
46
- puts "job row added, force enqueue runner, wait 300s"
51
+ puts "job row added, force enqueue runner, wait for stages"
47
52
  r.enqueue!
48
- sleep 300
53
+ wait_for_stages(900)
49
54
 
50
55
  puts "update job status and activity"
51
56
  r.update_gsheet(gdrive_slot)
@@ -61,4 +66,27 @@ describe "Mobilize" do
61
66
 
62
67
  end
63
68
 
69
+ def wait_for_stages(time_limit=600,stage_limit=120,wait_length=10)
70
+ time = 0
71
+ time_since_stage = 0
72
+ #check for 10 min
73
+ while time < time_limit and time_since_stage < stage_limit
74
+ sleep wait_length
75
+ job_classes = Mobilize::Resque.jobs.map{|j| j['class']}
76
+ if job_classes.include?("Mobilize::Stage")
77
+ time_since_stage = 0
78
+ puts "saw stage at #{time.to_s} seconds"
79
+ else
80
+ time_since_stage += wait_length
81
+ puts "#{time_since_stage.to_s} seconds since stage seen"
82
+ end
83
+ time += wait_length
84
+ puts "total wait time #{time.to_s} seconds"
85
+ end
86
+
87
+ if time >= time_limit
88
+ raise "Timed out before stage completion"
89
+ end
90
+ end
91
+
64
92
  end
@@ -2,17 +2,17 @@
2
2
  active: true
3
3
  trigger: once
4
4
  status: ""
5
- stage1: 'ssh.run node:"test_node", cmd:"ruby code.rb", user:"root", sources:["Runner_mobilize(test)/code.rb", "Runner_mobilize(test)/code.sh"]'
6
- stage2: 'gsheet.write source:"stage1", target:"Runner_mobilize(test)/test_ssh_1.out"'
5
+ stage1: 'ssh.run node:"test_node", cmd:"ruby code.rb", user:"root", sources:["code.rb", "code.sh"]'
6
+ stage2: 'gsheet.write source:"stage1", target:"test_ssh_1.out"'
7
7
  - name: test_ssh_2
8
8
  active: true
9
9
  trigger: "after test_ssh_1"
10
10
  status: ""
11
- stage1: 'ssh.run cmd:"sh code.sh", user:"root", source:"Runner_mobilize(test)/code.sh"'
12
- stage2: 'gsheet.write source:"stage1", target:"Runner_mobilize(test)/test_ssh_2.out"'
11
+ stage1: 'ssh.run cmd:"sh code2.sh", user:"root", sources:["code2.sh","test_node/var/log/syslog"]'
12
+ stage2: 'gsheet.write source:"stage1", target:"test_ssh_2.out"'
13
13
  - name: test_ssh_3
14
14
  active: true
15
15
  trigger: "after test_ssh_2"
16
16
  status: ""
17
17
  stage1: 'ssh.run cmd:"whoami"'
18
- stage2: 'gsheet.write source:"stage1", target:"Runner_mobilize(test)/test_ssh_3.out"'
18
+ stage2: 'gsheet.write source:"stage1", target:"test_ssh_3.out"'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mobilize-ssh
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.10
4
+ version: '1.2'
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-03-05 00:00:00.000000000 Z
12
+ date: 2013-03-21 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: mobilize-base
@@ -18,7 +18,7 @@ dependencies:
18
18
  requirements:
19
19
  - - '='
20
20
  - !ruby/object:Gem::Version
21
- version: 1.1.10
21
+ version: '1.2'
22
22
  type: :runtime
23
23
  prerelease: false
24
24
  version_requirements: !ruby/object:Gem::Requirement
@@ -26,7 +26,7 @@ dependencies:
26
26
  requirements:
27
27
  - - '='
28
28
  - !ruby/object:Gem::Version
29
- version: 1.1.10
29
+ version: '1.2'
30
30
  - !ruby/object:Gem::Dependency
31
31
  name: net-ssh
32
32
  requirement: !ruby/object:Gem::Requirement
@@ -77,7 +77,7 @@ dependencies:
77
77
  version: '0'
78
78
  description: mobilize-ssh allows you to automate ssh commands and files across hosts
79
79
  email:
80
- - cpaesleme@ngmoco.com
80
+ - cpaesleme@dena.com
81
81
  executables: []
82
82
  extensions: []
83
83
  extra_rdoc_files: []
@@ -99,6 +99,7 @@ files:
99
99
  - mobilize-ssh.gemspec
100
100
  - test/code.rb
101
101
  - test/code.sh
102
+ - test/code2.sh
102
103
  - test/mobilize-ssh_test.rb
103
104
  - test/redis-test.conf
104
105
  - test/ssh_job_rows.yml
@@ -130,6 +131,7 @@ summary: extend mobilize-base with the ability to run files across hosts
130
131
  test_files:
131
132
  - test/code.rb
132
133
  - test/code.sh
134
+ - test/code2.sh
133
135
  - test/mobilize-ssh_test.rb
134
136
  - test/redis-test.conf
135
137
  - test/ssh_job_rows.yml