ssh 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Binary file
@@ -0,0 +1,27 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'autotest/isolate'
4
+ require "autotest/restart"
5
+
6
+ Autotest.add_hook :initialize do |at|
7
+ at.testlib = "minitest/autorun"
8
+ at.add_exception "tmp"
9
+
10
+ # at.extra_files << "../some/external/dependency.rb"
11
+ #
12
+ # at.libs << ":../some/external"
13
+ #
14
+ # at.add_exception "vendor"
15
+ #
16
+ # at.add_mapping(/dependency.rb/) do |f, _|
17
+ # at.files_matching(/test_.*rb$/)
18
+ # end
19
+ #
20
+ # %w(TestA TestB).each do |klass|
21
+ # at.extra_class_map[klass] = "test/test_misc.rb"
22
+ # end
23
+ end
24
+
25
+ # Autotest.add_hook :run_command do |at|
26
+ # system "rake build"
27
+ # end
File without changes
@@ -0,0 +1,5 @@
1
+ === 1.0.0 / 2011-11-03
2
+
3
+ * 1 major enhancement
4
+
5
+ * Birthday! Sorta... Extracted from rake-remote_task 2.0.5.
@@ -0,0 +1,7 @@
1
+ .autotest
2
+ History.txt
3
+ Manifest.txt
4
+ README.txt
5
+ Rakefile
6
+ lib/ssh.rb
7
+ test/test_ssh.rb
@@ -0,0 +1,58 @@
1
+ = SSH
2
+
3
+ home :: https://github.com/seattlerb/ssh
4
+ rdoc :: http://docs.seattlerb.org/ssh
5
+
6
+ == DESCRIPTION:
7
+
8
+ SSH provides a simple streaming ssh command runner. That's it.
9
+ This is a one trick pony.
10
+
11
+ ssh = SSH.new "example.com", "/var/log"
12
+ puts ssh.run "ls"
13
+
14
+ SSH was extracted from rake-remote_task which was extracted from vlad.
15
+
16
+ == FEATURES/PROBLEMS:
17
+
18
+ * Provides a simple streaming ssh command runner.
19
+ * There is no 2
20
+ * How the hell was this gem name not used already?
21
+
22
+ == SYNOPSIS:
23
+
24
+ ssh = SSH.new "example.com", "/var/log"
25
+ puts ssh.run "ls"
26
+
27
+ == REQUIREMENTS:
28
+
29
+ * open4
30
+
31
+ == INSTALL:
32
+
33
+ * sudo gem install ssh
34
+
35
+ == LICENSE:
36
+
37
+ (The MIT License)
38
+
39
+ Copyright (c) Ryan Davis, seattle.rb
40
+
41
+ Permission is hereby granted, free of charge, to any person obtaining
42
+ a copy of this software and associated documentation files (the
43
+ 'Software'), to deal in the Software without restriction, including
44
+ without limitation the rights to use, copy, modify, merge, publish,
45
+ distribute, sublicense, and/or sell copies of the Software, and to
46
+ permit persons to whom the Software is furnished to do so, subject to
47
+ the following conditions:
48
+
49
+ The above copyright notice and this permission notice shall be
50
+ included in all copies or substantial portions of the Software.
51
+
52
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
53
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
54
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
55
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
56
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
57
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
58
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,16 @@
1
+ # -*- ruby -*-
2
+
3
+ require "rubygems"
4
+ require "hoe"
5
+
6
+ Hoe.plugin :isolate
7
+ Hoe.plugin :seattlerb
8
+
9
+ Hoe.spec "ssh" do
10
+ developer "Ryan Davis", "ryand-ruby@zenspider.com"
11
+
12
+ # dependency 'rake', '~> 0.8'
13
+ dependency 'open4', '~> 1.0'
14
+ end
15
+
16
+ # vim: syntax=ruby
@@ -0,0 +1,110 @@
1
+ require 'rubygems'
2
+ require 'open4'
3
+
4
+ ##
5
+ # SSH provides a simple streaming ssh command runner. That's it.
6
+ # This is a one trick pony.
7
+ #
8
+ # ssh = SSH.new "example.com", "/var/log"
9
+ # puts ssh.run "ls"
10
+ #
11
+ # SSH was extracted from rake-remote_task which was extracted from vlad.
12
+ #
13
+ # SSH's idea contributed by Joel Parker Henderson.
14
+
15
+ class SSH
16
+ VERSION = "1.0.0"
17
+
18
+ class Error < RuntimeError; end
19
+
20
+ class CommandFailedError < Error
21
+ attr_reader :status
22
+ def initialize status
23
+ @status = status
24
+ end
25
+ end
26
+
27
+ include Open4
28
+
29
+ attr_accessor :ssh_cmd, :ssh_flags, :target_host, :target_dir
30
+ attr_accessor :sudo_prompt, :sudo_password
31
+
32
+ def initialize target_host = nil, target_dir = nil
33
+ self.ssh_cmd = "ssh"
34
+ self.ssh_flags = []
35
+ self.target_host = target_host
36
+ self.target_dir = target_dir
37
+
38
+ self.sudo_prompt = /^Password:/
39
+ self.sudo_password = nil
40
+ end
41
+
42
+ def run command
43
+ command = "cd #{target_dir} && #{command}" if target_dir
44
+ cmd = [ssh_cmd, ssh_flags, target_host, command].flatten
45
+
46
+ if $DEBUG then
47
+ trace = [ssh_cmd, ssh_flags, target_host, "'#{command}'"]
48
+ warn trace.flatten.join ' '
49
+ end
50
+
51
+ pid, inn, out, err = popen4(*cmd)
52
+
53
+ status, result = empty_streams pid, inn, out, err
54
+
55
+ unless status.success? then
56
+ e = status.exitstatus
57
+ c = cmd.join ' '
58
+ raise(CommandFailedError.new(status), "Failed with status #{e}: #{c}")
59
+ end
60
+
61
+ result.join
62
+ ensure
63
+ inn.close rescue nil
64
+ out.close rescue nil
65
+ err.close rescue nil
66
+ end
67
+
68
+ def empty_streams pid, inn, out, err
69
+ result = []
70
+ inn.sync = true
71
+ streams = [out, err]
72
+ out_stream = {
73
+ out => $stdout,
74
+ err => $stderr,
75
+ }
76
+
77
+ # Handle process termination ourselves
78
+ status = nil
79
+ Thread.start do
80
+ status = Process.waitpid2(pid).last
81
+ end
82
+
83
+ until streams.empty? do
84
+ # don't busy loop
85
+ selected, = select streams, nil, nil, 0.1
86
+
87
+ next if selected.nil? or selected.empty?
88
+
89
+ selected.each do |stream|
90
+ if stream.eof? then
91
+ streams.delete stream if status # we've quit, so no more writing
92
+ next
93
+ end
94
+
95
+ data = stream.readpartial(1024)
96
+ out_stream[stream].write data
97
+
98
+ if stream == err and data =~ sudo_prompt then
99
+ inn.puts sudo_password
100
+ data << "\n"
101
+ $stderr.write "\n"
102
+ end
103
+
104
+ result << data
105
+ end
106
+ end
107
+
108
+ return status, result
109
+ end
110
+ end
@@ -0,0 +1,132 @@
1
+ require "minitest/autorun"
2
+ require "ssh"
3
+
4
+ require 'stringio'
5
+
6
+ class StringIO
7
+ def readpartial(size) read end # suck!
8
+ end
9
+
10
+ module Process
11
+ def self.expected status
12
+ @@expected ||= []
13
+ @@expected << status
14
+ end
15
+
16
+ class << self
17
+ alias :waitpid2_old :waitpid2
18
+
19
+ def waitpid2(pid)
20
+ [ @@expected.shift ]
21
+ end
22
+ end
23
+ end
24
+
25
+ class SSH
26
+ attr_accessor :commands, :action, :input, :output, :error
27
+
28
+ class Status < Struct.new :exitstatus
29
+ def success?() exitstatus == 0 end
30
+ end
31
+
32
+ def popen4 *command
33
+ @commands << command
34
+
35
+ @input = StringIO.new
36
+ out = StringIO.new @output.shift.to_s
37
+ err = StringIO.new @error.shift.to_s
38
+
39
+ raise if block_given?
40
+
41
+ status = self.action ? self.action[command.join(' ')] : 0
42
+ Process.expected Status.new(status)
43
+
44
+ return 42, @input, out, err
45
+ end
46
+
47
+ def select reads, writes, errs, timeout
48
+ [reads, writes, errs]
49
+ end
50
+ end
51
+
52
+ class TestSSH < MiniTest::Unit::TestCase
53
+ def setup
54
+ super
55
+ @ssh = SSH.new
56
+ @ssh.commands = []
57
+ @ssh.output = []
58
+ @ssh.error = []
59
+ @ssh.action = nil
60
+ end
61
+
62
+ def test_run
63
+ @ssh.output << "file1\nfile2\n"
64
+ @ssh.target_host = "app.example.com"
65
+ result = nil
66
+
67
+ out, err = capture_io do
68
+ result = @ssh.run("ls")
69
+ end
70
+
71
+ commands = @ssh.commands
72
+
73
+ assert_equal 1, commands.size, 'not enough commands'
74
+ assert_equal ["ssh", "app.example.com", "ls"],
75
+ commands.first, 'app'
76
+ assert_equal "file1\nfile2\n", result
77
+
78
+ assert_equal "file1\nfile2\n", out
79
+ assert_equal '', err
80
+ end
81
+
82
+ def test_run_dir
83
+ @ssh.target_host = "app.example.com"
84
+ @ssh.target_dir = "/www/dir1"
85
+
86
+ @ssh.run("ls")
87
+
88
+ commands = @ssh.commands
89
+
90
+ assert_equal [["ssh", "app.example.com", "cd /www/dir1 && ls"]], commands
91
+ end
92
+
93
+ def test_run_failing_command
94
+ @ssh.input = StringIO.new "file1\nfile2\n"
95
+ @ssh.target_host = 'app.example.com'
96
+ @ssh.action = proc { 1 }
97
+
98
+ e = assert_raises(SSH::CommandFailedError) { @ssh.run("ls") }
99
+ assert_equal "Failed with status 1: ssh app.example.com ls", e.message
100
+
101
+ assert_equal [["ssh", "app.example.com", "ls"]], @ssh.commands
102
+ end
103
+
104
+ def test_run_sudo
105
+ @ssh.output << "file1\nfile2\n"
106
+ @ssh.error << 'Password:'
107
+ @ssh.target_host = "app.example.com"
108
+ @ssh.sudo_password = "my password"
109
+ result = nil
110
+
111
+ out, err = capture_io do
112
+ result = @ssh.run("sudo ls")
113
+ end
114
+
115
+ commands = @ssh.commands
116
+
117
+ assert_equal 1, commands.size, 'not enough commands'
118
+ assert_equal ['ssh', 'app.example.com', 'sudo ls'],
119
+ commands.first
120
+
121
+ assert_equal "my password\n", @ssh.input.string
122
+
123
+ # WARN: Technically incorrect, the password line should be
124
+ # first... this is an artifact of changes to the IO code in run
125
+ # and the fact that we have a very simplistic (non-blocking)
126
+ # testing model.
127
+ assert_equal "file1\nfile2\nPassword:\n", result
128
+
129
+ assert_equal "file1\nfile2\n", out
130
+ assert_equal "Password:\n", err
131
+ end
132
+ end
metadata ADDED
@@ -0,0 +1,163 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ssh
3
+ version: !ruby/object:Gem::Version
4
+ hash: 23
5
+ prerelease:
6
+ segments:
7
+ - 1
8
+ - 0
9
+ - 0
10
+ version: 1.0.0
11
+ platform: ruby
12
+ authors:
13
+ - Ryan Davis
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain:
17
+ - |
18
+ -----BEGIN CERTIFICATE-----
19
+ MIIDPjCCAiagAwIBAgIBADANBgkqhkiG9w0BAQUFADBFMRMwEQYDVQQDDApyeWFu
20
+ ZC1ydWJ5MRkwFwYKCZImiZPyLGQBGRYJemVuc3BpZGVyMRMwEQYKCZImiZPyLGQB
21
+ GRYDY29tMB4XDTA5MDMwNjE4NTMxNVoXDTEwMDMwNjE4NTMxNVowRTETMBEGA1UE
22
+ AwwKcnlhbmQtcnVieTEZMBcGCgmSJomT8ixkARkWCXplbnNwaWRlcjETMBEGCgmS
23
+ JomT8ixkARkWA2NvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALda
24
+ b9DCgK+627gPJkB6XfjZ1itoOQvpqH1EXScSaba9/S2VF22VYQbXU1xQXL/WzCkx
25
+ taCPaLmfYIaFcHHCSY4hYDJijRQkLxPeB3xbOfzfLoBDbjvx5JxgJxUjmGa7xhcT
26
+ oOvjtt5P8+GSK9zLzxQP0gVLS/D0FmoE44XuDr3iQkVS2ujU5zZL84mMNqNB1znh
27
+ GiadM9GHRaDiaxuX0cIUBj19T01mVE2iymf9I6bEsiayK/n6QujtyCbTWsAS9Rqt
28
+ qhtV7HJxNKuPj/JFH0D2cswvzznE/a5FOYO68g+YCuFi5L8wZuuM8zzdwjrWHqSV
29
+ gBEfoTEGr7Zii72cx+sCAwEAAaM5MDcwCQYDVR0TBAIwADALBgNVHQ8EBAMCBLAw
30
+ HQYDVR0OBBYEFEfFe9md/r/tj/Wmwpy+MI8d9k/hMA0GCSqGSIb3DQEBBQUAA4IB
31
+ AQAY59gYvDxqSqgC92nAP9P8dnGgfZgLxP237xS6XxFGJSghdz/nI6pusfCWKM8m
32
+ vzjjH2wUMSSf3tNudQ3rCGLf2epkcU13/rguI88wO6MrE0wi4ZqLQX+eZQFskJb/
33
+ w6x9W1ur8eR01s397LSMexySDBrJOh34cm2AlfKr/jokKCTwcM0OvVZnAutaovC0
34
+ l1SVZ0ecg88bsWHA0Yhh7NFxK1utWoIhtB6AFC/+trM0FQEB/jZkIS8SaNzn96Rl
35
+ n0sZEf77FLf5peR8TP/PtmIg7Cyqz23sLM4mCOoTGIy5OcZ8TdyiyINUHtb5ej/T
36
+ FBHgymkyj/AOSqKRIpXPhjC6
37
+ -----END CERTIFICATE-----
38
+
39
+ date: 2011-11-15 00:00:00 Z
40
+ dependencies:
41
+ - !ruby/object:Gem::Dependency
42
+ name: open4
43
+ prerelease: false
44
+ requirement: &id001 !ruby/object:Gem::Requirement
45
+ none: false
46
+ requirements:
47
+ - - ~>
48
+ - !ruby/object:Gem::Version
49
+ hash: 15
50
+ segments:
51
+ - 1
52
+ - 0
53
+ version: "1.0"
54
+ type: :runtime
55
+ version_requirements: *id001
56
+ - !ruby/object:Gem::Dependency
57
+ name: rdoc
58
+ prerelease: false
59
+ requirement: &id002 !ruby/object:Gem::Requirement
60
+ none: false
61
+ requirements:
62
+ - - ~>
63
+ - !ruby/object:Gem::Version
64
+ hash: 21
65
+ segments:
66
+ - 3
67
+ - 9
68
+ version: "3.9"
69
+ type: :development
70
+ version_requirements: *id002
71
+ - !ruby/object:Gem::Dependency
72
+ name: minitest
73
+ prerelease: false
74
+ requirement: &id003 !ruby/object:Gem::Requirement
75
+ none: false
76
+ requirements:
77
+ - - ~>
78
+ - !ruby/object:Gem::Version
79
+ hash: 13
80
+ segments:
81
+ - 2
82
+ - 7
83
+ version: "2.7"
84
+ type: :development
85
+ version_requirements: *id003
86
+ - !ruby/object:Gem::Dependency
87
+ name: hoe
88
+ prerelease: false
89
+ requirement: &id004 !ruby/object:Gem::Requirement
90
+ none: false
91
+ requirements:
92
+ - - ~>
93
+ - !ruby/object:Gem::Version
94
+ hash: 27
95
+ segments:
96
+ - 2
97
+ - 12
98
+ version: "2.12"
99
+ type: :development
100
+ version_requirements: *id004
101
+ description: |-
102
+ SSH provides a simple streaming ssh command runner. That's it.
103
+ This is a one trick pony.
104
+
105
+ ssh = SSH.new "example.com", "/var/log"
106
+ puts ssh.run "ls"
107
+
108
+ SSH was extracted from rake-remote_task which was extracted from vlad.
109
+ email:
110
+ - ryand-ruby@zenspider.com
111
+ executables: []
112
+
113
+ extensions: []
114
+
115
+ extra_rdoc_files:
116
+ - History.txt
117
+ - Manifest.txt
118
+ - README.txt
119
+ files:
120
+ - .autotest
121
+ - History.txt
122
+ - Manifest.txt
123
+ - README.txt
124
+ - Rakefile
125
+ - lib/ssh.rb
126
+ - test/test_ssh.rb
127
+ - .gemtest
128
+ homepage: https://github.com/seattlerb/ssh
129
+ licenses: []
130
+
131
+ post_install_message:
132
+ rdoc_options:
133
+ - --main
134
+ - README.txt
135
+ require_paths:
136
+ - lib
137
+ required_ruby_version: !ruby/object:Gem::Requirement
138
+ none: false
139
+ requirements:
140
+ - - ">="
141
+ - !ruby/object:Gem::Version
142
+ hash: 3
143
+ segments:
144
+ - 0
145
+ version: "0"
146
+ required_rubygems_version: !ruby/object:Gem::Requirement
147
+ none: false
148
+ requirements:
149
+ - - ">="
150
+ - !ruby/object:Gem::Version
151
+ hash: 3
152
+ segments:
153
+ - 0
154
+ version: "0"
155
+ requirements: []
156
+
157
+ rubyforge_project: ssh
158
+ rubygems_version: 1.8.10
159
+ signing_key:
160
+ specification_version: 3
161
+ summary: SSH provides a simple streaming ssh command runner
162
+ test_files:
163
+ - test/test_ssh.rb
Binary file