mobilize-ssh 1.1.10 → 1.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.
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